Skip to content

Commit fe0e6c9

Browse files
perf: improve parse performance for url() functions
1 parent f5f21ea commit fe0e6c9

File tree

2 files changed

+83
-137
lines changed

2 files changed

+83
-137
lines changed

src/plugins/postcss-url-parser.js

+58-106
Original file line numberDiff line numberDiff line change
@@ -59,128 +59,80 @@ function walkUrls(parsed, callback) {
5959
});
6060
}
6161

62-
function getUrlsFromValue(value, result, filter, decl) {
63-
if (!needParseDecl.test(value)) {
64-
return;
65-
}
66-
67-
const parsed = valueParser(value);
68-
const urls = [];
69-
70-
walkUrls(parsed, (node, url, needQuotes, isStringValue) => {
71-
if (url.trim().replace(/\\[\r\n]/g, '').length === 0) {
72-
result.warn(`Unable to find uri in '${decl ? decl.toString() : value}'`, {
73-
node: decl,
74-
});
75-
76-
return;
77-
}
62+
export default postcss.plugin(pluginName, (options) => (css, result) => {
63+
const importsMap = new Map();
64+
const replacersMap = new Map();
7865

79-
if (filter && !filter(url)) {
66+
css.walkDecls((decl) => {
67+
if (!needParseDecl.test(decl.value)) {
8068
return;
8169
}
8270

83-
const splittedUrl = url.split(/(\?)?#/);
84-
const [urlWithoutHash, singleQuery, hashValue] = splittedUrl;
85-
const hash =
86-
singleQuery || hashValue
87-
? `${singleQuery ? '?' : ''}${hashValue ? `#${hashValue}` : ''}`
88-
: '';
89-
90-
const normalizedUrl = normalizeUrl(urlWithoutHash, isStringValue);
91-
92-
urls.push({ node, url: normalizedUrl, hash, needQuotes });
93-
});
94-
95-
// eslint-disable-next-line consistent-return
96-
return { parsed, urls };
97-
}
98-
99-
function walkDecls(css, result, filter) {
100-
const items = [];
71+
const parsed = valueParser(decl.value);
10172

102-
css.walkDecls((decl) => {
103-
const item = getUrlsFromValue(decl.value, result, filter, decl);
73+
walkUrls(parsed, (node, url, needQuotes, isStringValue) => {
74+
if (url.trim().replace(/\\[\r\n]/g, '').length === 0) {
75+
result.warn(
76+
`Unable to find uri in '${decl ? decl.toString() : decl.value}'`,
77+
{ node: decl }
78+
);
10479

105-
if (!item || item.urls.length === 0) {
106-
return;
107-
}
80+
return;
81+
}
10882

109-
items.push({ decl, parsed: item.parsed, urls: item.urls });
110-
});
83+
if (options.filter && !options.filter(url)) {
84+
return;
85+
}
11186

112-
return items;
113-
}
87+
const splittedUrl = url.split(/(\?)?#/);
88+
const [urlWithoutHash, singleQuery, hashValue] = splittedUrl;
89+
const hash =
90+
singleQuery || hashValue
91+
? `${singleQuery ? '?' : ''}${hashValue ? `#${hashValue}` : ''}`
92+
: '';
11493

115-
function flatten(array) {
116-
return array.reduce((a, b) => a.concat(b), []);
117-
}
94+
const normalizedUrl = normalizeUrl(urlWithoutHash, isStringValue);
11895

119-
function collectUniqueUrlsWithNodes(array) {
120-
return array.reduce((accumulator, currentValue) => {
121-
const { url, needQuotes, hash, node } = currentValue;
122-
const found = accumulator.find(
123-
(item) =>
124-
url === item.url && needQuotes === item.needQuotes && hash === item.hash
125-
);
126-
127-
if (!found) {
128-
accumulator.push({ url, hash, needQuotes, nodes: [node] });
129-
} else {
130-
found.nodes.push(node);
131-
}
96+
const importKey = normalizedUrl;
97+
let importName = importsMap.get(importKey);
13298

133-
return accumulator;
134-
}, []);
135-
}
99+
if (!importName) {
100+
importName = `___CSS_LOADER_URL_IMPORT_${importsMap.size}___`;
101+
importsMap.set(importKey, importName);
136102

137-
export default postcss.plugin(
138-
pluginName,
139-
(options) =>
140-
function process(css, result) {
141-
const traversed = walkDecls(css, result, options.filter);
142-
const flattenTraversed = flatten(traversed.map((item) => item.urls));
143-
const urlsWithNodes = collectUniqueUrlsWithNodes(flattenTraversed);
144-
const replacers = new Map();
145-
146-
urlsWithNodes.forEach((urlWithNodes, index) => {
147-
const { url, hash, needQuotes, nodes } = urlWithNodes;
148-
const replacementName = `___CSS_LOADER_URL_REPLACEMENT_${index}___`;
149-
150-
result.messages.push(
151-
{
152-
pluginName,
153-
type: 'import',
154-
value: { type: 'url', replacementName, url, needQuotes, hash },
103+
result.messages.push({
104+
pluginName,
105+
type: 'import',
106+
value: {
107+
type: 'url',
108+
importName,
109+
url: normalizedUrl,
155110
},
156-
{
157-
pluginName,
158-
type: 'replacer',
159-
value: { type: 'url', replacementName },
160-
}
161-
);
162-
163-
nodes.forEach((node) => {
164-
replacers.set(node, replacementName);
165111
});
166-
});
112+
}
167113

168-
traversed.forEach((item) => {
169-
walkUrls(item.parsed, (node) => {
170-
const replacementName = replacers.get(node);
114+
const replacerKey = JSON.stringify({ importKey, hash, needQuotes });
171115

172-
if (!replacementName) {
173-
return;
174-
}
116+
let replacerName = replacersMap.get(replacerKey);
175117

176-
// eslint-disable-next-line no-param-reassign
177-
node.type = 'word';
178-
// eslint-disable-next-line no-param-reassign
179-
node.value = replacementName;
118+
if (!replacerName) {
119+
replacerName = `___CSS_LOADER_URL_REPLACEMENT_${replacersMap.size}___`;
120+
replacersMap.set(replacerKey, replacerName);
121+
122+
result.messages.push({
123+
pluginName,
124+
type: 'replacer',
125+
value: { type: 'url', replacerName, importName, hash, needQuotes },
180126
});
127+
}
181128

182-
// eslint-disable-next-line no-param-reassign
183-
item.decl.value = item.parsed.toString();
184-
});
185-
}
186-
);
129+
// eslint-disable-next-line no-param-reassign
130+
node.type = 'word';
131+
// eslint-disable-next-line no-param-reassign
132+
node.value = replacerName;
133+
});
134+
135+
// eslint-disable-next-line no-param-reassign
136+
decl.value = parsed.toString();
137+
});
138+
});

src/utils.js

+25-31
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,8 @@ function getImportCode(
206206
const importItems = [];
207207
const codeItems = [];
208208
const atRuleImportNames = new Map();
209-
const urlImportNames = new Map();
210209

210+
let hasUrlHelper = false;
211211
let importPrefix;
212212

213213
if (exportType === 'full') {
@@ -273,7 +273,7 @@ function getImportCode(
273273
break;
274274
case 'url':
275275
{
276-
if (urlImportNames.size === 0) {
276+
if (!hasUrlHelper) {
277277
const helperUrl = stringifyRequest(
278278
loaderContext,
279279
require.resolve('./runtime/getUrl.js')
@@ -284,33 +284,16 @@ function getImportCode(
284284
? `import ___CSS_LOADER_GET_URL_IMPORT___ from ${helperUrl};`
285285
: `var ___CSS_LOADER_GET_URL_IMPORT___ = require(${helperUrl});`
286286
);
287+
hasUrlHelper = true;
287288
}
288289

289-
const { replacementName, url, hash, needQuotes } = item;
290+
const { importName, url } = item;
291+
const importUrl = stringifyRequest(loaderContext, url);
290292

291-
let importName = urlImportNames.get(url);
292-
293-
if (!importName) {
294-
const importUrl = stringifyRequest(loaderContext, url);
295-
296-
importName = `___CSS_LOADER_URL_IMPORT_${urlImportNames.size}___`;
297-
importItems.push(
298-
esModule
299-
? `import ${importName} from ${importUrl};`
300-
: `var ${importName} = require(${importUrl});`
301-
);
302-
303-
urlImportNames.set(url, importName);
304-
}
305-
306-
const getUrlOptions = []
307-
.concat(hash ? [`hash: ${JSON.stringify(hash)}`] : [])
308-
.concat(needQuotes ? 'needQuotes: true' : []);
309-
const preparedOptions =
310-
getUrlOptions.length > 0 ? `, { ${getUrlOptions.join(', ')} }` : '';
311-
312-
codeItems.push(
313-
`var ${replacementName} = ___CSS_LOADER_GET_URL_IMPORT___(${importName}${preparedOptions});`
293+
importItems.push(
294+
esModule
295+
? `import ${importName} from ${importUrl};`
296+
: `var ${importName} = require(${importUrl});`
314297
);
315298
}
316299
break;
@@ -359,19 +342,30 @@ function getModuleCode(
359342
const sourceMapValue = sourceMap && map ? `,${map}` : '';
360343

361344
let cssCode = JSON.stringify(css);
345+
let replacersCode = '';
362346

363347
replacers.forEach((replacer) => {
364-
const { type, replacementName } = replacer;
348+
const { type } = replacer;
365349

366350
if (type === 'url') {
351+
const { replacerName, importName, hash, needQuotes } = replacer;
352+
353+
const getUrlOptions = []
354+
.concat(hash ? [`hash: ${JSON.stringify(hash)}`] : [])
355+
.concat(needQuotes ? 'needQuotes: true' : []);
356+
const preparedOptions =
357+
getUrlOptions.length > 0 ? `, { ${getUrlOptions.join(', ')} }` : '';
358+
359+
replacersCode += `var ${replacerName} = ___CSS_LOADER_GET_URL_IMPORT___(${importName}${preparedOptions});\n`;
360+
367361
cssCode = cssCode.replace(
368-
new RegExp(replacementName, 'g'),
369-
() => `" + ${replacementName} + "`
362+
new RegExp(replacerName, 'g'),
363+
() => `" + ${replacerName} + "`
370364
);
371365
}
372366

373367
if (type === 'icss-import') {
374-
const { importName, localName } = replacer;
368+
const { importName, localName, replacementName } = replacer;
375369

376370
cssCode = cssCode.replace(
377371
new RegExp(replacementName, 'g'),
@@ -380,7 +374,7 @@ function getModuleCode(
380374
}
381375
});
382376

383-
return `// Module\nexports.push([module.id, ${cssCode}, ""${sourceMapValue}]);\n`;
377+
return `${replacersCode}// Module\nexports.push([module.id, ${cssCode}, ""${sourceMapValue}]);\n`;
384378
}
385379

386380
function dashesCamelCase(str) {

0 commit comments

Comments
 (0)