Environment
- uniwind version:
1.0.4
- Platform: Web (React + Expo Router)
- Framework: React 19, React Native Web
Description
We're experiencing severe performance degradation (7+ second blocking times) when using uniwind alongside Emotion CSS-in-JS. The root cause is that cssListener.js's MutationObserver reprocesses all stylesheets from scratch every time a new stylesheet is added to <head>.
When Emotion runs in dev mode, it injects individual <style data-emotion> tags for each CSS rule. In our case, this results in 2000+ style tags, each triggering a full reprocessing cycle.
Root Cause
In cssListener.js, the MutationObserver calls initialize() on every childList mutation:
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === "childList") {
this.initialize(); // ⚠️ Reprocesses ALL stylesheets
}
}
});
The initialize() method iterates through all document.styleSheets without tracking which ones have already been processed, creating O(n²) behavior:
initialize() {
for (const sheet of Array.from(document.styleSheets)) {
// No check if already processed
this.addMediaQueriesDeep(rules);
}
}
Performance Profile
From Chrome DevTools profiling a single navigation:
addMediaQueriesDeep: 3,600ms (51.3%)
initialize: 751ms (10.7%)
addMediaQuery: 337ms (4.8%)
collectParentMediaQueries: 301ms (4.3%)
Total blocking time from cssListener: ~4.5 seconds (65% of 7s navigation)
Suggested Solutions
1. Debounce MutationObserver (low-hanging fruit)
Batch multiple rapid mutations into a single initialize() call:
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === "childList") {
if (this.initializeTimeout) clearTimeout(this.initializeTimeout);
this.initializeTimeout = setTimeout(() => {
this.initialize();
this.initializeTimeout = null;
}, 50);
}
}
});
2. Track processed stylesheets (more impactful)
Skip stylesheets that have already been processed:
processedStyleSheets = new WeakSet();
initialize() {
for (const sheet of Array.from(document.styleSheets)) {
if (this.processedStyleSheets.has(sheet)) continue;
// ... process stylesheet ...
this.processedStyleSheets.add(sheet);
}
}
3. Ignore Emotion style tags (alternative)
If uniwind doesn't need to track [data-emotion] styles, they could be filtered:
initialize() {
for (const sheet of Array.from(document.styleSheets)) {
if (sheet.ownerNode?.hasAttribute('data-emotion')) continue;
// ... process stylesheet ...
}
}
Workaround
We've temporarily patched our local node_modules/uniwind with solutions #1 and #2, which reduced navigation time from 7s to <1s. We've also enabled Emotion's speedy mode to eliminate the style tag proliferation.
Impact
This affects any uniwind + CSS-in-JS (Emotion, styled-components in dev mode) setup where many style tags are injected dynamically. It's particularly severe in micro-frontend architectures where multiple apps coexist.
Would you be open to a PR implementing one or more of these optimizations?
Environment
1.0.4Description
We're experiencing severe performance degradation (7+ second blocking times) when using uniwind alongside Emotion CSS-in-JS. The root cause is that
cssListener.js's MutationObserver reprocesses all stylesheets from scratch every time a new stylesheet is added to<head>.When Emotion runs in dev mode, it injects individual
<style data-emotion>tags for each CSS rule. In our case, this results in 2000+ style tags, each triggering a full reprocessing cycle.Root Cause
In
cssListener.js, the MutationObserver callsinitialize()on everychildListmutation:The
initialize()method iterates through alldocument.styleSheetswithout tracking which ones have already been processed, creating O(n²) behavior:Performance Profile
From Chrome DevTools profiling a single navigation:
addMediaQueriesDeep: 3,600ms (51.3%)initialize: 751ms (10.7%)addMediaQuery: 337ms (4.8%)collectParentMediaQueries: 301ms (4.3%)Total blocking time from cssListener: ~4.5 seconds (65% of 7s navigation)
Suggested Solutions
1. Debounce MutationObserver (low-hanging fruit)
Batch multiple rapid mutations into a single
initialize()call:2. Track processed stylesheets (more impactful)
Skip stylesheets that have already been processed:
3. Ignore Emotion style tags (alternative)
If uniwind doesn't need to track
[data-emotion]styles, they could be filtered:Workaround
We've temporarily patched our local
node_modules/uniwindwith solutions #1 and #2, which reduced navigation time from 7s to <1s. We've also enabled Emotion'sspeedymode to eliminate the style tag proliferation.Impact
This affects any uniwind + CSS-in-JS (Emotion, styled-components in dev mode) setup where many style tags are injected dynamically. It's particularly severe in micro-frontend architectures where multiple apps coexist.
Would you be open to a PR implementing one or more of these optimizations?