Skip to content

v2.68.33

Latest

Choose a tag to compare

@github-actions github-actions released this 08 Oct 21:35
Immutable release. Only release title and notes can be modified.
e270229
  • Header now-playing pop-in (collage). The card was the whole content of a PPR hole: getLatestSong uses cacheLife('seconds'), and per the cacheLife docs an expire under 5 minutes makes a dynamic hole. So the shell shipped .chrome__nowPlaying empty and the card streamed in about 1.3s later, pushing the header and page down. This is now the standard Partial Prerendering split. The shell renders the card from getLastKnownSong (same fetch, cacheLife('minutes'), so it gets prerendered). The live song still streams in through a Suspense hole, as PublishNowPlaying inside <Suspense fallback={null}>. useNowPlaying only reads now, so the shell's older track can't overwrite a newer published one. Page widgets keep publishing through usePublishNowPlaying like before.
  • Classic header is unchanged on purpose. It still streams the card into a Suspense hole (<template id="B:1"> in the header), so the card shows up about 300ms after first paint. It measures CLS 0 because classic's header is a single row: the card only widens the logo pill, and nothing moves. Classic never solved the pop. Its layout just hides it, so there was nothing to align the collage with.
  • MUI CSS shipped twice in every prerendered <head>. With cacheComponents, Next's prerenderToStream runs a throwaway warmup client prerender, then the final one. Both go through the same ServerInsertedHTMLProvider, which every other warmup path in app-render gives its own throwaway provider. Each render mounted its own AppRouterCacheProvider Emotion registry, and both callbacks fired into the final head. Instrumented build: 1 channel, 2 registries, each flushing the full shell CSS. This is still the case in Next 16.4.0 and 16.5.0-canary.4. @mui/material-nextjs 9.4.0 (latest) has no option for it: enableCssLayer only wraps rules in @layer mui. EmotionStyleRegistry replaces AppRouterCacheProvider and renders the same <style data-emotion> tags. It tracks which style names it already streamed per request, keyed on the request's ServerInsertedHTMLContext, so the second registry skips them. This drops @mui/material-nextjs.
  • Favorite albums LCP (collage). The first two mobile rows (4 covers) hold the LCP but were loading="lazy" with auto priority. They now load eagerly with fetchpriority="high". Classic is unchanged.
  • Tests cover these cases:
    • The live track wins over the shell track.
    • The shell track never clobbers a published one.
    • Two renders sharing one request stream each style once. This test fails without the dedupe.
    • Collage loads the first screen eagerly; classic stays lazy.

Measurements

Production build (next build && next start) with INTERACTIVE_REDESIGN=1 and MAP_TILES=offline, using local Contentful/Spotify stubs. Medians of 3 runs.

Layout shift (Puppeteer, 390px mobile, unthrottled). cardAt is when the header card first paints.

Route CLS before CLS after cardAt before cardAt after
/ 0.027 0 ~1350ms in shell
/music 0.027 0 1241ms 436ms
/music/albums?album=… 0.027 0 1360ms 440ms
/music/albums 0.027 0 1287ms 438ms

Before, the shifted nodes were nav.chrome__nav, main.collageMain, and div.collageBackdrop, at exactly cardAt. After, no shifts on any route. Classic stays at CLS 0 in the header, with cardAt at about 650ms on /music and /music/albums.

Prerendered HTML, main → this PR (KB). Raw sizes, with gzip and brotli for the whole document.

Surface Route <head> HTML gzip brotli
collage / 143.3 → 96.5 425.0 → 373.2 72.8 → 66.7 36.8 → 37.0
collage /music 125.9 → 86.1 451.4 → 404.7 68.0 → 61.2 33.2 → 33.2
collage album 125.0 → 96.1 586.1 → 550.8 72.0 → 66.8 35.7 → 35.8
collage /music/albums 125.0 → 96.1 541.1 → 505.8 68.4 → 62.4 33.6 → 33.7
classic / 350.6 → 176.4 563.1 → 389.0 56.3 → 40.8 26.4 → 26.3
classic /music 286.6 → 142.7 448.8 → 304.9 43.2 → 31.1 21.3 → 21.2
classic album 327.5 → 167.1 648.7 → 488.4 53.3 → 40.5 26.5 → 26.5
classic /music/albums 327.5 → 167.1 591.5 → 431.0 48.7 → 35.4 23.7 → 23.5

Every head now has exactly one copy of each Emotion style. Collage heads still contain the header card's styles, which main streams later. Brotli already hid the repeat on the wire, so the gain is half the CSS for the browser to parse plus 11–16 KB less under gzip.

Mobile Lighthouse (simulated throttling).

Route Perf CLS LCP TBT JS KB LCP discovery
/ 92 → 93 0.027 → 0 3253 → 3244ms 70 → 58ms 305 → 305 3/3 → 3/3
/music 93 → 90 0.027 → 0 3088 → 3577ms 71 → 62ms 313 → 313 text LCP
album 87 → 88 0.027 → 0 3794 → 3771ms 173 → 95ms 345 → 345 1/3 → 3/3
/music/albums 88 → 87 0.027 → 0 3736 → 3774ms 150 → 156ms 345 → 345 1/3 → 3/3

On the album grid routes, observed LCP resource load delay drops from 64–71ms to 9–19ms. Simulated LCP and Perf move within run-to-run noise (the /music LCP run spread was 3085–3630ms on base too). TTFB is 4–7ms everywhere, because the static shell is prerendered. Lighthouse ran before the head CSS fix.

Looked at, not changed

  • Simulated LCP (~3.2–3.8s) comes from throttled JS boot of 305–345 KB, not from the server or the shell. Cutting it means cutting client JS (MUI/Emotion runtime), which is out of scope here.
  • Style overlap between the shell and the request-time render. The request-time render starts a fresh registry, so up to ~8 KB of rules the shell already shipped get re-sent in the body. That was already there on main. Fixing it needs the shell's style list to survive into the request-time render.
  • No render-blocking font or CSS requests, and no dynamic hole holding up the shell, now that the header card is out of its hole.

Caveats

All numbers come from stub data, so real Spotify artwork sizes and Contentful latency are unverified. The stale-then-live swap only shows when the minutes cache lags the seconds cache, and on stubs both return the same track.

Classic (flag off) renders the same as main, checked three ways:

  • The <body> markup is identical on 5 routes; the only text change is the footer release version from the bot bump.
  • Every style from main's final head copy is still present. The dropped names came only from the warmup render, and no element uses them.
  • Mobile screenshots are pixel-identical. Desktop screenshots differ only in an animated icon, which differs the same way between two loads of main.

Checks: pnpm exec turbo check:ci check:types test passes (28/28 tasks; web: 66 suites, 292 tests).