[2.x] fix(perf): replace async-CSS dual path with render-blocking stylesheet#4664
Merged
Conversation
…lesheet The dual-path strategy from #4561 (sessionStorage warm cache + async preload cold path) relied on JS-injected <link rel="stylesheet"> elements blocking paint, which they do not by spec. The result was a flash of unstyled content on cross-origin asset hosts (S3 / CloudFront most visibly), on every cold tab, and intermittently on warm visits too. - makeHead() now emits a standard <link rel="stylesheet"> for forum/admin CSS. Parser-discovered, render-blocking, ~single-digit ms on a CDN. - The sessionStorage warm-path script and the noscript fallback are removed; the inline critical CSS stays as a safety net so the body has a theme-accurate background while the stylesheet is in flight. - JS preloads switch from fetchpriority="low" to "high" — now that the stylesheet blocks paint, the SPA boot is the next critical-path item. - New Document::$preHead bucket for content that must render before the stylesheet (used by the inline data-theme script so dark-mode users never see a light-flash if the browser paints before CSS arrives). Document::$head[] semantics are unchanged: items still render after the stylesheet, so admin-supplied <style>/<link> overrides correctly win the cascade.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
PR #4561 introduced a sessionStorage-driven warm CSS cache plus async preload cold path, intended to eliminate FOUC while keeping CSS off the critical path. In practice the strategy was broken at the spec level: a JS-injected
<link rel="stylesheet">doesn't render-block the way a parser-discovered one does. The result was a visible flash of unstyled content on every cold tab (sessionStorage is per-tab) and intermittently on warm visits too, especially when assets are served from a cross-origin host (S3+CloudFront the most visible case).This replaces the dual path with the standard
<link rel="stylesheet">. Parser-discovered, render-blocking, ~single-digit ms on a CDN, no flash. The inline critical CSS stays as a safety net for the brief window before the stylesheet is parsed.Changes
Document::makeHead()emits a plain<link rel="stylesheet" href="…" fetchpriority="high">per CSS URL. The sessionStorage inline script, the async preload tag, and the<noscript>fallback are gone.fetchpriority="low"to"high"— the stylesheet is now the first paint blocker, so the SPA boot (forum.js) is the next critical-path item.Document::$preHead[]field for content that must render before the main stylesheet — used by the inline data-theme script so dark-mode users don't briefly see a light background if the browser paints before the stylesheet applies.$head[]semantics are unchanged: items still render after the stylesheet, preserving cascade order for extension-supplied<style>/<link>overrides.app.blade.php.For extension authors
Document::makeHead()has changed. Test snapshots that captured the previous async-preload markup will need regenerating.Document::$head[]semantics are unchanged — items continue to render after the main stylesheet, so admin-supplied custom CSS still wins the cascade.Document::$preHead[]is available for the rare case where head content genuinely needs to be in effect before first paint (e.g. an inline script setting attributes on<html>that critical CSS keys off). Existing extensions don't need to change.Test plan
fetchpriority="high",data-themeinline script precedes the stylesheet.ianm/html-headextension: inserted a test<style>row, confirmed it renders after the main stylesheet and would correctly override forum.css. No regression for admin-customised head content.