Skip to content

Fix a css order violation#89615

Open
lukesandberg wants to merge 2 commits intocanaryfrom
style_order_across_chunks
Open

Fix a css order violation#89615
lukesandberg wants to merge 2 commits intocanaryfrom
style_order_across_chunks

Conversation

@lukesandberg
Copy link
Contributor

@lukesandberg lukesandberg commented Feb 6, 2026

What

When a component imports and overrides styles from another component, the CSS cascade order could be violated in production builds. For example:

CardWrapper.module.css (sets background: purple)
  └── imports Card.module.css (sets background: white)

The expected behavior is that CardWrapper's purple background overrides Card's white background. However, in production builds, the shared chunk containing Card's CSS could be emitted before CardWrapper's CSS, causing the wrong color to appear.

Root Cause

The style chunking algorithm groups CSS modules into shared batches to reduce HTTP requests. The bug was that shared batches were emitted at the position of their first member in the topological order, rather than their last member.

Consider this dependency graph with post-order traversal [C, B, A] where A depends on B:

A → B
C

If C and A are grouped into a shared batch, the old code would emit that batch when it first saw C - before B. This violates the constraint that A must come after B.

The Fix

style_production.rs: Track how many items remain for each shared batch. Only emit a batch when all its members have been seen (i.e., at the position of its last member).

style_groups.rs:

  • Use FxIndexMap for chunk_group_indices to ensure deterministic iteration order
  • Minor cleanups (use FxHashSet directly instead of collecting to Vec)
  • Fix how module_dependents were computed to be more accurate. The prior approach could infer more dependencies than there actually were

Fixes #89523
Fixes #83941
Fixes #86704
Fixes #88604
Fixes #87321

For a client reference we embed all the css dependencies in the page in the original order of the chunks

The order of the chunk_items is based on a DFS post order traversal of the client reference entry point, however the chunks could end up in a different order based on how _shared_ chunks were computed.

Consider these three items

A ->B
C

A post order traversal could reasonably be either
1: `[B, A, C]`
2: `[C, B, A]`
3: `[B, C, A]`

The key thing about this graph is that `A` must come after `B` to preserve the dependency order.  However if `C` and `A` get grouped together as a shared group then depending on the original post order list we might create chunks in the wrong output order!

In particular if we have `[C, B, A]` and C and A get put in a shared group, then it will be the _first_ chunk we emit (and thus serve).

The fix is to output shared chunks only once we have seen all of their members in the post order traversal.
@nextjs-bot nextjs-bot added created-by: Turbopack team PRs by the Turbopack team. Turbopack Related to Turbopack with Next.js. labels Feb 6, 2026
Copy link
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@nextjs-bot
Copy link
Collaborator

nextjs-bot commented Feb 6, 2026

Tests Passed

@codspeed-hq
Copy link

codspeed-hq bot commented Feb 6, 2026

Merging this PR will not alter performance

✅ 17 untouched benchmarks
⏩ 3 skipped benchmarks1


Comparing style_order_across_chunks (a737077) with canary (7cd9c70)

Open in CodSpeed

Footnotes

  1. 3 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@lukesandberg lukesandberg marked this pull request as ready for review February 6, 2026 23:08
@lukesandberg lukesandberg requested a review from mischnic February 6, 2026 23:08
@Netail
Copy link
Contributor

Netail commented Feb 6, 2026

Might wanna add #87321 & #88604 as related issues

@nextjs-bot
Copy link
Collaborator

Stats from current PR

✅ No significant changes detected

📊 All Metrics
📖 Metrics Glossary

Dev Server Metrics:

  • Listen = TCP port starts accepting connections
  • First Request = HTTP server returns successful response
  • Cold = Fresh build (no cache)
  • Warm = With cached build artifacts

Build Metrics:

  • Fresh = Clean build (no .next directory)
  • Cached = With existing .next directory

Change Thresholds:

  • Time: Changes < 50ms AND < 10%, OR < 2% are insignificant
  • Size: Changes < 1KB AND < 1% are insignificant
  • All other changes are flagged to catch regressions

⚡ Dev Server

Metric Canary PR Change Trend
Cold (Listen) 558ms 558ms ▁▁▁█▁
Cold (Ready in log) 545ms 549ms ▂▂▂█▁
Cold (First Request) 1.098s 1.103s ▃▃▃█▄
Warm (Listen) 559ms 559ms ▁▁▁█▁
Warm (Ready in log) 542ms 542ms ▁▁▁█▁
Warm (First Request) 439ms 441ms ▁▁▂█▂
📦 Dev Server (Webpack) (Legacy)

📦 Dev Server (Webpack)

Metric Canary PR Change Trend
Cold (Listen) 455ms 456ms ▁▄▁▅▁
Cold (Ready in log) 438ms 438ms ▃▄▄▄▄
Cold (First Request) 1.849s 1.845s ▂▂▂▄▂
Warm (Listen) 456ms 455ms ▁▄▁▄▁
Warm (Ready in log) 437ms 437ms ▃▃▂▄▄
Warm (First Request) 1.854s 1.838s ▂▂▂▄▃

⚡ Production Builds

Metric Canary PR Change Trend
Fresh Build 5.120s 5.112s ▁▁▁█▁
Cached Build 5.085s 5.052s ▁▁▁█▁
📦 Production Builds (Webpack) (Legacy)

📦 Production Builds (Webpack)

Metric Canary PR Change Trend
Fresh Build 13.866s 13.899s ▁▂▁▄▂
Cached Build 13.953s 13.983s ▁▂▁▃▂
node_modules Size 467 MB 467 MB █████
📦 Bundle Sizes

Bundle Sizes

⚡ Turbopack

Client

Main Bundles: **437 kB** → **437 kB** ⚠️ +13 B

81 files with content-based hashes (individual files not comparable between builds)

Server

Middleware
Canary PR Change
middleware-b..fest.js gzip 766 B 762 B
Total 766 B 762 B ✅ -4 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 451 B 450 B
Total 451 B 450 B ✅ -1 B

📦 Webpack

Client

Main Bundles
Canary PR Change
5528-HASH.js gzip 5.47 kB N/A -
6280-HASH.js gzip 56.9 kB N/A -
6335.HASH.js gzip 169 B N/A -
912-HASH.js gzip 4.53 kB N/A -
e8aec2e4-HASH.js gzip 62.5 kB N/A -
framework-HASH.js gzip 59.7 kB 59.7 kB
main-app-HASH.js gzip 256 B 254 B
main-HASH.js gzip 39.1 kB 39.1 kB
webpack-HASH.js gzip 1.68 kB 1.68 kB
262-HASH.js gzip N/A 4.52 kB -
2889.HASH.js gzip N/A 169 B -
5602-HASH.js gzip N/A 5.48 kB -
6948ada0-HASH.js gzip N/A 62.5 kB -
9544-HASH.js gzip N/A 57.5 kB -
Total 230 kB 231 kB ⚠️ +613 B
Polyfills
Canary PR Change
polyfills-HASH.js gzip 39.4 kB 39.4 kB
Total 39.4 kB 39.4 kB
Pages
Canary PR Change
_app-HASH.js gzip 194 B 194 B
_error-HASH.js gzip 183 B 180 B 🟢 3 B (-2%)
css-HASH.js gzip 331 B 330 B
dynamic-HASH.js gzip 1.81 kB 1.81 kB
edge-ssr-HASH.js gzip 256 B 256 B
head-HASH.js gzip 351 B 352 B
hooks-HASH.js gzip 384 B 383 B
image-HASH.js gzip 580 B 581 B
index-HASH.js gzip 260 B 260 B
link-HASH.js gzip 2.49 kB 2.49 kB
routerDirect..HASH.js gzip 320 B 319 B
script-HASH.js gzip 386 B 386 B
withRouter-HASH.js gzip 315 B 315 B
1afbb74e6ecf..834.css gzip 106 B 106 B
Total 7.97 kB 7.97 kB ✅ -1 B

Server

Edge SSR
Canary PR Change
edge-ssr.js gzip 126 kB 126 kB
page.js gzip 249 kB 249 kB
Total 375 kB 375 kB ⚠️ +467 B
Middleware
Canary PR Change
middleware-b..fest.js gzip 615 B 615 B
middleware-r..fest.js gzip 156 B 155 B
middleware.js gzip 33 kB 33.2 kB
edge-runtime..pack.js gzip 842 B 842 B
Total 34.6 kB 34.8 kB ⚠️ +178 B
Build Details
Build Manifests
Canary PR Change
_buildManifest.js gzip 732 B 736 B
Total 732 B 736 B ⚠️ +4 B
Build Cache
Canary PR Change
0.pack gzip 3.84 MB 3.85 MB 🔴 +11.2 kB (+0%)
index.pack gzip 102 kB 103 kB
index.pack.old gzip 104 kB 103 kB
Total 4.05 MB 4.06 MB ⚠️ +10.5 kB

🔄 Shared (bundler-independent)

Runtimes
Canary PR Change
app-page-exp...dev.js gzip 315 kB 315 kB
app-page-exp..prod.js gzip 167 kB 167 kB
app-page-tur...dev.js gzip 315 kB 315 kB
app-page-tur..prod.js gzip 167 kB 167 kB
app-page-tur...dev.js gzip 312 kB 312 kB
app-page-tur..prod.js gzip 165 kB 165 kB
app-page.run...dev.js gzip 312 kB 312 kB
app-page.run..prod.js gzip 165 kB 165 kB
app-route-ex...dev.js gzip 70.5 kB 70.5 kB
app-route-ex..prod.js gzip 49 kB 49 kB
app-route-tu...dev.js gzip 70.5 kB 70.5 kB
app-route-tu..prod.js gzip 49 kB 49 kB
app-route-tu...dev.js gzip 70.1 kB 70.1 kB
app-route-tu..prod.js gzip 48.8 kB 48.8 kB
app-route.ru...dev.js gzip 70.1 kB 70.1 kB
app-route.ru..prod.js gzip 48.7 kB 48.7 kB
dist_client_...dev.js gzip 324 B 324 B
dist_client_...dev.js gzip 326 B 326 B
dist_client_...dev.js gzip 318 B 318 B
dist_client_...dev.js gzip 317 B 317 B
pages-api-tu...dev.js gzip 43.2 kB 43.2 kB
pages-api-tu..prod.js gzip 32.9 kB 32.9 kB
pages-api.ru...dev.js gzip 43.1 kB 43.1 kB
pages-api.ru..prod.js gzip 32.8 kB 32.8 kB
pages-turbo....dev.js gzip 52.5 kB 52.5 kB
pages-turbo...prod.js gzip 39.4 kB 39.4 kB
pages.runtim...dev.js gzip 52.5 kB 52.5 kB
pages.runtim..prod.js gzip 39.3 kB 39.3 kB
server.runti..prod.js gzip 62.7 kB 62.7 kB
Total 2.8 MB 2.8 MB ✅ -2 B

@icyJoseph
Copy link
Collaborator

Is this one also solved by this PR? #86459

@lukesandberg lukesandberg changed the title Fix an css order violation Fix a css order violation Feb 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

created-by: Turbopack team PRs by the Turbopack team. Turbopack Related to Turbopack with Next.js.

Projects

None yet

4 participants