如何通过编程方式智能合并冗余CSS声明以优化网站加载速度
Great question—this is a common but often overlooked optimization when cleaning up CSS! The key here is to merge duplicate selectors while respecting the CSS cascade (later declarations override earlier ones for the same property, just like the browser would interpret them).
Here are two elegant approaches to handle this at scale:
Solution 1: Use a Mature PostCSS Plugin (Easiest Approach)
PostCSS has a dedicated plugin called postcss-merge-rules that does exactly what you need—it merges duplicate selectors, preserves all unique properties, and applies the cascade correctly. It’s battle-tested and works seamlessly with thousands of CSS rules.
Steps to implement:
- Install PostCSS and the plugin:
npm install postcss postcss-merge-rules --save-dev - Create a
postcss.config.jsfile in your project root:module.exports = { plugins: [ require('postcss-merge-rules')({ // Optional: Configure if needed (defaults work for most cases) removeDuplicates: true }) ] }; - Run PostCSS on your CSS file(s). For example, using
postcss-cli:npx postcss input.css -o output.css
For your sample CSS, this will output:
#header { width: 800px; background-color: #777; } h2 { font-size: 24px; } h3 { font-size: 16px; }
Solution 2: Build a Custom Script with CSS Parsing Libraries (For Full Control)
If you need custom logic (like handling edge cases specific to your codebase), use a CSS AST parser like css-tree or postcss to build your own merging tool. This gives you full control over how selectors are merged and properties are prioritized.
Example with css-tree:
css-tree is a fast, robust CSS parser that makes it easy to traverse and manipulate CSS ASTs.
- Install
css-tree:npm install css-tree --save-dev - Create a script (e.g.,
merge-css.js):const csstree = require('css-tree'); const fs = require('fs'); // Read your input CSS file const rawCss = fs.readFileSync('input.css', 'utf8'); // Parse CSS into an Abstract Syntax Tree (AST) const ast = csstree.parse(rawCss); // Map to store merged declarations per selector const mergedSelectors = new Map(); // Traverse the AST to collect and merge declarations csstree.walk(ast, { enter(node) { if (node.type === 'Rule') { // Convert selector(s) to a normalized string const selectorKey = csstree.generate(node.prelude); // Initialize entry if selector doesn't exist if (!mergedSelectors.has(selectorKey)) { mergedSelectors.set(selectorKey, new Map()); } const declarations = mergedSelectors.get(selectorKey); // Add/update declarations: later ones override earlier ones node.block.children.forEach(child => { if (child.type === 'Declaration') { declarations.set(child.property, child); } }); } } }); // Build a new AST from merged selectors const newAst = { type: 'StyleSheet', children: [] }; mergedSelectors.forEach((decls, selector) => { const rule = { type: 'Rule', prelude: csstree.parse(selector, { context: 'selector' }), block: { type: 'Block', children: Array.from(decls.values()) } }; newAst.children.push(rule); }); // Generate the final merged CSS const mergedCss = csstree.generate(newAst); // Write to output file fs.writeFileSync('output.css', mergedCss, 'utf8'); console.log('CSS merged successfully!'); - Run the script:
node merge-css.js
Key Notes:
- This script respects the CSS cascade: if a property is declared multiple times for the same selector, the last occurrence wins.
- It handles complex selectors (e.g.,
.class1, .class2,#header .nav-link) correctly, sincecss-treenormalizes selector strings. - You can extend it to handle edge cases like
!importantrules, media queries, or nested selectors by adjusting the AST traversal logic.
内容的提问来源于stack exchange,提问作者Bangkokian

