the page. The red bars indicate bytes that never matched a DOM element. Export the unused selector list. You will typically find three categories:
- Orphaned component styles from deprecated features
- Responsive overrides for breakpoints that no longer exist in the design system
- Framework utility classes imported wholesale but used sparingly
Delete the unused selectors. Verify the application renders correctly. This step alone typically removes 25-35% of payload.
Example: Pre-optimization component stylesheet
/* Legacy dashboard layout */
.dashboard-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
.dashboard-card { background: #f8f9fa; border: 1px solid #e2e8f0; border-radius: 0.5rem; padding: 1.5rem; }
.dashboard-card--featured { border-color: #3b82f6; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); }
.dashboard-card--collapsed { display: none; }
.dashboard-sidebar { width: 240px; padding: 1rem; background: #1e293b; }
.dashboard-sidebar--mobile { display: none; }
Example: Post-optimization (dead code removed)
/* Active dashboard layout only */
.dashboard-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
.dashboard-card { background: #f8f9fa; border: 1px solid #e2e8f0; border-radius: 0.5rem; padding: 1.5rem; }
.dashboard-card--featured { border-color: #3b82f6; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); }
Architecture Rationale: Dead code elimination is prioritized first because it carries zero risk of cascade interference. Removing unused selectors reduces the browser's selector matching workload immediately. It also simplifies subsequent consolidation steps by shrinking the search space.
Step 2: Declaration Consolidation & Selector Refactoring
Identify property-value pairs that repeat across multiple selectors. Group them into a shared rule. This reduces raw byte count and simplifies the cascade.
Example: Before consolidation
.alert-container { padding: 1rem; border-radius: 0.375rem; border: 1px solid transparent; }
.notification-panel { padding: 1rem; border-radius: 0.375rem; border: 1px solid transparent; }
.tooltip-wrapper { padding: 1rem; border-radius: 0.375rem; border: 1px solid transparent; }
Example: After consolidation
.alert-container,
.notification-panel,
.tooltip-wrapper {
padding: 1rem;
border-radius: 0.375rem;
border: 1px solid transparent;
}
Architecture Rationale: Consolidation trades selector specificity for declaration reuse. The browser evaluates grouped selectors once per matching element, reducing style computation overhead. However, avoid over-consolidation. If selectors serve fundamentally different semantic purposes, grouping them creates implicit coupling. Use this technique for visual twins, not conceptual cousins.
Step 3: Tokenization via Custom Properties
Replace hardcoded values with CSS custom properties. This eliminates repetition, enables runtime theme switching, and centralizes design tokens.
Example: Token definition
:root {
--spacing-unit: 0.25rem;
--spacing-sm: calc(var(--spacing-unit) * 2);
--spacing-md: calc(var(--spacing-unit) * 4);
--radius-base: 0.375rem;
--radius-lg: 0.5rem;
--border-default: 1px solid #e2e8f0;
}
Example: Tokenized component
.alert-container {
padding: var(--spacing-md);
border-radius: var(--radius-base);
border: var(--border-default);
}
.notification-panel {
padding: var(--spacing-md);
border-radius: var(--radius-base);
border: var(--border-default);
}
Architecture Rationale: Custom properties outperform preprocessor variables (Sass/Less) because they cascade, inherit, and evaluate at runtime. This enables dynamic theming without rebuilds. Tokenization also reduces byte count by replacing repeated literals with short variable references. The browser's CSS parser handles variable resolution efficiently, and modern engines cache computed values.
Step 4: Logical Property Migration & Gzip Validation
Replace physical direction properties (margin-left, padding-right, border-top) with logical equivalents (margin-inline-start, padding-inline-end, border-block-start). This prepares the stylesheet for internationalization and right-to-left layouts without media query overrides.
Example: Logical property usage
.card-layout {
margin-inline-start: var(--spacing-md);
padding-block-end: var(--spacing-sm);
border-inline-end: var(--border-default);
}
Architecture Rationale: Logical properties reduce the need for RTL-specific overrides, shrinking stylesheet size and maintenance burden. They are natively supported in all modern browsers. After applying all optimizations, validate the final payload using gzip compression. Raw byte reduction does not always translate to network savings. Gzip relies on repetitive patterns; aggressive consolidation can sometimes reduce compression efficiency. Always compare .css.gz sizes before and after.
Pitfall Guide
1. The Gzip Blind Spot
Explanation: Developers optimize for raw byte reduction without checking compressed size. Merging selectors or removing repetition can degrade gzip efficiency, resulting in a larger network payload despite a smaller file.
Fix: Always measure .gz or .br (Brotli) sizes. Use build tools that report both raw and compressed metrics. Prioritize techniques that preserve or increase repetitive patterns for compression.
2. Specificity Inflation via Over-Nesting
Explanation: Deep nesting (nav > ul > li > a > span) creates long selectors that are difficult to override. Developers respond by adding more specific rules or !important, triggering a specificity arms race that bloats the stylesheet.
Fix: Cap nesting at two levels. Use flat class names (BEM, utility classes, or CSS modules) to maintain predictable specificity. Rely on cascade order, not selector weight, for styling precedence.
3. Variable Sprawl
Explanation: Creating custom properties for every single value defeats the purpose of tokenization. It increases stylesheet size, complicates maintenance, and offers no runtime benefit.
Fix: Tokenize only values repeated three or more times, or values that require dynamic theming. Keep one-off values inline. Audit variables quarterly and remove unused tokens.
4. Blind Framework Stripping
Explanation: Manually removing framework classes or imports without coverage analysis breaks responsive layouts, interactive states, or accessibility attributes. Frameworks often bundle interdependent rules.
Fix: Run automated coverage tools before deleting framework code. Use purge utilities that analyze HTML/JSX templates to safely remove unused classes. Test responsive breakpoints and dark mode states after removal.
5. Ignoring Cascade Order
Explanation: Consolidating rules or reordering selectors can change declaration precedence. A rule that previously won specificity might now lose, causing visual regressions.
Fix: Group consolidated rules logically. Maintain source order consistency. Run visual regression tests after structural changes. Document intentional cascade dependencies.
6. Logical Property Fallback Neglect
Explanation: Older browsers or specific rendering environments may lack full logical property support. Unhandled fallbacks cause layout shifts or missing spacing in legacy clients.
Fix: Use PostCSS plugins like postcss-logical or postcss-dir-pseudo-class to generate fallbacks. Provide physical property fallbacks in the cascade: margin-left: 1rem; margin-inline-start: 1rem;.
7. Over-Consolidation of Semantic Styles
Explanation: Grouping selectors that share visual properties but serve different semantic purposes creates implicit coupling. Future design changes require splitting rules, increasing maintenance overhead.
Fix: Consolidate only visual twins. Preserve semantic boundaries. Use utility classes for shared visual patterns instead of merging component selectors.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Legacy codebase with 60% unused CSS | Dead selector elimination + automated purge | Highest immediate ROI; removes technical debt | Low engineering cost; high performance gain |
| Design system with repeated spacing/colors | Custom property tokenization | Enables runtime theming; reduces repetition | Medium upfront effort; long-term maintenance savings |
| Multi-language app requiring RTL support | Logical property migration | Eliminates RTL override stylesheets | Low cost; prevents future bloat |
| Utility framework heavily customized | Selective import + coverage pruning | Removes unused framework classes safely | Low cost; reduces initial payload significantly |
| High-traffic marketing site | Consolidation + gzip validation | Maximizes network compression efficiency | Medium effort; direct FCP/LCP improvement |
Configuration Template
Use this PostCSS configuration to automate logical property fallbacks, custom property validation, and unused CSS detection during build time.
// postcss.config.js
module.exports = {
plugins: [
require('postcss-logical')({
preserve: false, // Generates fallbacks and removes logical properties for legacy support
}),
require('postcss-custom-properties')({
preserve: true, // Keeps custom properties for runtime theming
warnings: true, // Alerts on unused or undefined variables
}),
require('postcss-purgecss')({
content: ['./src/**/*.html', './src/**/*.jsx', './src/**/*.tsx'],
defaultExtractor: (content) => content.match(/[\w-/:]+(?<!:)/g) || [],
safelist: {
standard: [/^is-/, /^has-/, /^data-/],
deep: [/\.modal/, /\.tooltip/],
},
}),
require('cssnano')({
preset: ['default', {
discardComments: { removeAll: true },
normalizeWhitespace: true,
mergeRules: true,
minifyFontValues: { removeQuotes: false },
}],
}),
],
};
Quick Start Guide
- Initialize measurement: Open Chrome DevTools β Coverage β Enable "JS" and "CSS" β Start capturing β Reload page β Export unused CSS list.
- Prune dead code: Delete exported selectors. Run application. Verify no visual regressions. Record raw and gzip size.
- Tokenize repetition: Identify values repeated 3+ times. Replace with
:root custom properties. Update component rules to reference variables.
- Consolidate twins: Search for identical property blocks. Group selectors. Verify cascade order remains intact.
- Validate compression: Run build with gzip/Brotli enabled. Compare final payload size. Commit changes with performance metrics in PR description.