From fe0e6c94d0dc98432ab5af44ad1d44cf7f8870b6 Mon Sep 17 00:00:00 2001 From: Evilebot Tnawi Date: Fri, 20 Mar 2020 15:50:51 +0300 Subject: [PATCH] perf: improve parse performance for `url()` functions --- src/plugins/postcss-url-parser.js | 164 +++++++++++------------------- src/utils.js | 56 +++++----- 2 files changed, 83 insertions(+), 137 deletions(-) diff --git a/src/plugins/postcss-url-parser.js b/src/plugins/postcss-url-parser.js index b693542b..29424c42 100644 --- a/src/plugins/postcss-url-parser.js +++ b/src/plugins/postcss-url-parser.js @@ -59,128 +59,80 @@ function walkUrls(parsed, callback) { }); } -function getUrlsFromValue(value, result, filter, decl) { - if (!needParseDecl.test(value)) { - return; - } - - const parsed = valueParser(value); - const urls = []; - - walkUrls(parsed, (node, url, needQuotes, isStringValue) => { - if (url.trim().replace(/\\[\r\n]/g, '').length === 0) { - result.warn(`Unable to find uri in '${decl ? decl.toString() : value}'`, { - node: decl, - }); - - return; - } +export default postcss.plugin(pluginName, (options) => (css, result) => { + const importsMap = new Map(); + const replacersMap = new Map(); - if (filter && !filter(url)) { + css.walkDecls((decl) => { + if (!needParseDecl.test(decl.value)) { return; } - const splittedUrl = url.split(/(\?)?#/); - const [urlWithoutHash, singleQuery, hashValue] = splittedUrl; - const hash = - singleQuery || hashValue - ? `${singleQuery ? '?' : ''}${hashValue ? `#${hashValue}` : ''}` - : ''; - - const normalizedUrl = normalizeUrl(urlWithoutHash, isStringValue); - - urls.push({ node, url: normalizedUrl, hash, needQuotes }); - }); - - // eslint-disable-next-line consistent-return - return { parsed, urls }; -} - -function walkDecls(css, result, filter) { - const items = []; + const parsed = valueParser(decl.value); - css.walkDecls((decl) => { - const item = getUrlsFromValue(decl.value, result, filter, decl); + walkUrls(parsed, (node, url, needQuotes, isStringValue) => { + if (url.trim().replace(/\\[\r\n]/g, '').length === 0) { + result.warn( + `Unable to find uri in '${decl ? decl.toString() : decl.value}'`, + { node: decl } + ); - if (!item || item.urls.length === 0) { - return; - } + return; + } - items.push({ decl, parsed: item.parsed, urls: item.urls }); - }); + if (options.filter && !options.filter(url)) { + return; + } - return items; -} + const splittedUrl = url.split(/(\?)?#/); + const [urlWithoutHash, singleQuery, hashValue] = splittedUrl; + const hash = + singleQuery || hashValue + ? `${singleQuery ? '?' : ''}${hashValue ? `#${hashValue}` : ''}` + : ''; -function flatten(array) { - return array.reduce((a, b) => a.concat(b), []); -} + const normalizedUrl = normalizeUrl(urlWithoutHash, isStringValue); -function collectUniqueUrlsWithNodes(array) { - return array.reduce((accumulator, currentValue) => { - const { url, needQuotes, hash, node } = currentValue; - const found = accumulator.find( - (item) => - url === item.url && needQuotes === item.needQuotes && hash === item.hash - ); - - if (!found) { - accumulator.push({ url, hash, needQuotes, nodes: [node] }); - } else { - found.nodes.push(node); - } + const importKey = normalizedUrl; + let importName = importsMap.get(importKey); - return accumulator; - }, []); -} + if (!importName) { + importName = `___CSS_LOADER_URL_IMPORT_${importsMap.size}___`; + importsMap.set(importKey, importName); -export default postcss.plugin( - pluginName, - (options) => - function process(css, result) { - const traversed = walkDecls(css, result, options.filter); - const flattenTraversed = flatten(traversed.map((item) => item.urls)); - const urlsWithNodes = collectUniqueUrlsWithNodes(flattenTraversed); - const replacers = new Map(); - - urlsWithNodes.forEach((urlWithNodes, index) => { - const { url, hash, needQuotes, nodes } = urlWithNodes; - const replacementName = `___CSS_LOADER_URL_REPLACEMENT_${index}___`; - - result.messages.push( - { - pluginName, - type: 'import', - value: { type: 'url', replacementName, url, needQuotes, hash }, + result.messages.push({ + pluginName, + type: 'import', + value: { + type: 'url', + importName, + url: normalizedUrl, }, - { - pluginName, - type: 'replacer', - value: { type: 'url', replacementName }, - } - ); - - nodes.forEach((node) => { - replacers.set(node, replacementName); }); - }); + } - traversed.forEach((item) => { - walkUrls(item.parsed, (node) => { - const replacementName = replacers.get(node); + const replacerKey = JSON.stringify({ importKey, hash, needQuotes }); - if (!replacementName) { - return; - } + let replacerName = replacersMap.get(replacerKey); - // eslint-disable-next-line no-param-reassign - node.type = 'word'; - // eslint-disable-next-line no-param-reassign - node.value = replacementName; + if (!replacerName) { + replacerName = `___CSS_LOADER_URL_REPLACEMENT_${replacersMap.size}___`; + replacersMap.set(replacerKey, replacerName); + + result.messages.push({ + pluginName, + type: 'replacer', + value: { type: 'url', replacerName, importName, hash, needQuotes }, }); + } - // eslint-disable-next-line no-param-reassign - item.decl.value = item.parsed.toString(); - }); - } -); + // eslint-disable-next-line no-param-reassign + node.type = 'word'; + // eslint-disable-next-line no-param-reassign + node.value = replacerName; + }); + + // eslint-disable-next-line no-param-reassign + decl.value = parsed.toString(); + }); +}); diff --git a/src/utils.js b/src/utils.js index aba6a445..7b9a21dd 100644 --- a/src/utils.js +++ b/src/utils.js @@ -206,8 +206,8 @@ function getImportCode( const importItems = []; const codeItems = []; const atRuleImportNames = new Map(); - const urlImportNames = new Map(); + let hasUrlHelper = false; let importPrefix; if (exportType === 'full') { @@ -273,7 +273,7 @@ function getImportCode( break; case 'url': { - if (urlImportNames.size === 0) { + if (!hasUrlHelper) { const helperUrl = stringifyRequest( loaderContext, require.resolve('./runtime/getUrl.js') @@ -284,33 +284,16 @@ function getImportCode( ? `import ___CSS_LOADER_GET_URL_IMPORT___ from ${helperUrl};` : `var ___CSS_LOADER_GET_URL_IMPORT___ = require(${helperUrl});` ); + hasUrlHelper = true; } - const { replacementName, url, hash, needQuotes } = item; + const { importName, url } = item; + const importUrl = stringifyRequest(loaderContext, url); - let importName = urlImportNames.get(url); - - if (!importName) { - const importUrl = stringifyRequest(loaderContext, url); - - importName = `___CSS_LOADER_URL_IMPORT_${urlImportNames.size}___`; - importItems.push( - esModule - ? `import ${importName} from ${importUrl};` - : `var ${importName} = require(${importUrl});` - ); - - urlImportNames.set(url, importName); - } - - const getUrlOptions = [] - .concat(hash ? [`hash: ${JSON.stringify(hash)}`] : []) - .concat(needQuotes ? 'needQuotes: true' : []); - const preparedOptions = - getUrlOptions.length > 0 ? `, { ${getUrlOptions.join(', ')} }` : ''; - - codeItems.push( - `var ${replacementName} = ___CSS_LOADER_GET_URL_IMPORT___(${importName}${preparedOptions});` + importItems.push( + esModule + ? `import ${importName} from ${importUrl};` + : `var ${importName} = require(${importUrl});` ); } break; @@ -359,19 +342,30 @@ function getModuleCode( const sourceMapValue = sourceMap && map ? `,${map}` : ''; let cssCode = JSON.stringify(css); + let replacersCode = ''; replacers.forEach((replacer) => { - const { type, replacementName } = replacer; + const { type } = replacer; if (type === 'url') { + const { replacerName, importName, hash, needQuotes } = replacer; + + const getUrlOptions = [] + .concat(hash ? [`hash: ${JSON.stringify(hash)}`] : []) + .concat(needQuotes ? 'needQuotes: true' : []); + const preparedOptions = + getUrlOptions.length > 0 ? `, { ${getUrlOptions.join(', ')} }` : ''; + + replacersCode += `var ${replacerName} = ___CSS_LOADER_GET_URL_IMPORT___(${importName}${preparedOptions});\n`; + cssCode = cssCode.replace( - new RegExp(replacementName, 'g'), - () => `" + ${replacementName} + "` + new RegExp(replacerName, 'g'), + () => `" + ${replacerName} + "` ); } if (type === 'icss-import') { - const { importName, localName } = replacer; + const { importName, localName, replacementName } = replacer; cssCode = cssCode.replace( new RegExp(replacementName, 'g'), @@ -380,7 +374,7 @@ function getModuleCode( } }); - return `// Module\nexports.push([module.id, ${cssCode}, ""${sourceMapValue}]);\n`; + return `${replacersCode}// Module\nexports.push([module.id, ${cssCode}, ""${sourceMapValue}]);\n`; } function dashesCamelCase(str) {