@@ -17,7 +17,7 @@ import * as envConfig from '../next-server/lib/runtime-config'
1717import { getURL , loadGetInitialProps , ST } from '../next-server/lib/utils'
1818import type { NEXT_DATA } from '../next-server/lib/utils'
1919import initHeadManager from './head-manager'
20- import PageLoader , { createLink } from './page-loader'
20+ import PageLoader , { StyleSheetTuple } from './page-loader'
2121import measureWebVitals from './performance-relayer'
2222import { createRouter , makePublicRouterInstance } from './router'
2323
@@ -84,13 +84,25 @@ if (hasBasePath(asPath)) {
8484
8585type RegisterFn = ( input : [ string , ( ) => void ] ) => void
8686
87+ const looseToArray = < T extends { } > ( input : any ) : T [ ] => [ ] . slice . call ( input )
88+
8789const pageLoader = new PageLoader (
8890 buildId ,
8991 prefix ,
9092 page ,
91- [ ] . slice
92- . call ( document . querySelectorAll ( 'link[rel=stylesheet][data-n-p]' ) )
93- . map ( ( e : HTMLLinkElement ) => e . getAttribute ( 'href' ) ! )
93+ looseToArray < CSSStyleSheet > ( document . styleSheets )
94+ . filter (
95+ ( el : CSSStyleSheet ) =>
96+ el . ownerNode &&
97+ ( el . ownerNode as Element ) . tagName === 'LINK' &&
98+ ( el . ownerNode as Element ) . hasAttribute ( 'data-n-p' )
99+ )
100+ . map ( ( sheet ) => ( {
101+ href : ( sheet . ownerNode as Element ) . getAttribute ( 'href' ) ! ,
102+ text : looseToArray < CSSRule > ( sheet . cssRules )
103+ . map ( ( r ) => r . cssText )
104+ . join ( '' ) ,
105+ } ) )
94106)
95107const register : RegisterFn = ( [ r , f ] ) => pageLoader . registerPage ( r , f )
96108if ( window . __NEXT_P ) {
@@ -109,7 +121,7 @@ let lastRenderReject: (() => void) | null
109121let webpackHMR : any
110122export let router : Router
111123let CachedComponent : React . ComponentType
112- let cachedStyleSheets : string [ ]
124+ let cachedStyleSheets : StyleSheetTuple [ ]
113125let CachedApp : AppComponent , onPerfEntry : ( metric : any ) => void
114126
115127class Container extends React . Component < {
@@ -574,8 +586,8 @@ function doRender({
574586 // lastAppProps has to be set before ReactDom.render to account for ReactDom throwing an error.
575587 lastAppProps = appProps
576588
589+ let canceled = false
577590 let resolvePromise : ( ) => void
578- let renderPromiseReject : ( ) => void
579591 const renderPromise = new Promise ( ( resolve , reject ) => {
580592 if ( lastRenderReject ) {
581593 lastRenderReject ( )
@@ -584,7 +596,8 @@ function doRender({
584596 lastRenderReject = null
585597 resolve ( )
586598 }
587- renderPromiseReject = lastRenderReject = ( ) => {
599+ lastRenderReject = ( ) => {
600+ canceled = true
588601 lastRenderReject = null
589602
590603 const error : any = new Error ( 'Cancel rendering route' )
@@ -593,12 +606,9 @@ function doRender({
593606 }
594607 } )
595608
596- // TODO: consider replacing this with real `<style>` tags that have
597- // plain-text CSS content that's provided by RouteInfo. That'd remove the
598- // need for the staging `<link>`s and the ability for CSS to be missing at
599- // this phase, allowing us to remove the error handling flow that reloads the
600- // page.
601- function onStart ( ) : Promise < void [ ] > {
609+ // This function has a return type to ensure it doesn't start returning a
610+ // Promise. It should remain synchronous.
611+ function onStart ( ) : boolean {
602612 if (
603613 // We can skip this during hydration. Running it wont cause any harm, but
604614 // we may as well save the CPU cycles.
@@ -607,78 +617,27 @@ function doRender({
607617 // unless we're in production:
608618 process . env . NODE_ENV !== 'production'
609619 ) {
610- return Promise . resolve ( [ ] )
620+ return false
611621 }
612622
613- // Clean up previous render if canceling:
614- ; ( [ ] . slice . call (
615- document . querySelectorAll (
616- 'link[data-n-staging], noscript[data-n-staging]'
617- )
618- ) as HTMLLinkElement [ ] ) . forEach ( ( el ) => {
619- el . parentNode ! . removeChild ( el )
620- } )
621-
622- const referenceNodes : HTMLLinkElement [ ] = [ ] . slice . call (
623- document . querySelectorAll ( 'link[data-n-g], link[data-n-p]' )
624- ) as HTMLLinkElement [ ]
625- const referenceHrefs = new Set (
626- referenceNodes . map ( ( e ) => e . getAttribute ( 'href' ) )
623+ const currentStyleTags = looseToArray < HTMLStyleElement > (
624+ document . querySelectorAll ( 'style[data-n-href]' )
625+ )
626+ const currentHrefs = new Set (
627+ currentStyleTags . map ( ( tag ) => tag . getAttribute ( 'data-n-href' ) )
627628 )
628- let referenceNode : Element | undefined =
629- referenceNodes [ referenceNodes . length - 1 ]
630-
631- const required : ( Promise < any > | true ) [ ] = styleSheets . map ( ( href ) => {
632- let newNode : Element , promise : Promise < any > | true
633- const existingLink = referenceHrefs . has ( href )
634- if ( existingLink ) {
635- newNode = document . createElement ( 'noscript' )
636- newNode . setAttribute ( 'data-n-staging' , href )
637- promise = true
638- } else {
639- const [ link , onload ] = createLink ( href , 'stylesheet' )
640- link . setAttribute ( 'data-n-staging' , '' )
641- // Media `none` does not work in Firefox, so `print` is more
642- // cross-browser. Since this is so short lived we don't have to worry
643- // about style thrashing in a print view (where no routing is going to be
644- // happening anyway).
645- link . setAttribute ( 'media' , 'print' )
646- newNode = link
647- promise = onload
648- }
649629
650- if ( referenceNode ) {
651- referenceNode . parentNode ! . insertBefore (
652- newNode ,
653- referenceNode . nextSibling
654- )
655- referenceNode = newNode
656- } else {
657- document . head . appendChild ( newNode )
630+ styleSheets . forEach ( ( { href , text } ) => {
631+ if ( ! currentHrefs . has ( href ) ) {
632+ const styleTag = document . createElement ( 'style' )
633+ styleTag . setAttribute ( 'data-n-href' , href )
634+ styleTag . setAttribute ( 'media' , 'x' )
635+
636+ document . head . appendChild ( styleTag )
637+ styleTag . appendChild ( document . createTextNode ( text ) )
658638 }
659- return promise
660- } )
661- return Promise . all ( required ) . catch ( ( ) => {
662- // This is too late in the rendering lifecycle to use the existing
663- // `PAGE_LOAD_ERROR` flow (via `handleRouteInfoError`).
664- // To match that behavior, we request the page to reload with the current
665- // asPath. This is already set at this phase since we "committed" to the
666- // render.
667- // This handles an edge case where a new deployment is rolled during
668- // client-side transition and the CSS assets are missing.
669-
670- // This prevents:
671- // 1. An unstyled page from being rendered (old behavior)
672- // 2. The `/_error` page being rendered (we want to reload for the new
673- // deployment)
674- window . location . href = router . asPath
675-
676- // Instead of rethrowing the CSS loading error, we give a promise that
677- // won't resolve. This pauses the rendering process until the page
678- // reloads. Re-throwing the error could result in a flash of error page.
679- // throw cssLoadingError
680- return new Promise ( ( ) => { } )
681639 } )
640+ return true
682641 }
683642
684643 function onCommit ( ) {
@@ -689,40 +648,58 @@ function doRender({
689648 // We can skip this during hydration. Running it wont cause any harm, but
690649 // we may as well save the CPU cycles:
691650 ! isInitialRender &&
692- // Ensure this render commit owns the currently staged stylesheets:
693- renderPromiseReject === lastRenderReject
651+ // Ensure this render was not canceled
652+ ! canceled
694653 ) {
695- // Remove or relocate old stylesheets:
696- const relocatePlaceholders = [ ] . slice . call (
697- document . querySelectorAll ( 'noscript[data-n-staging]' )
698- ) as HTMLElement [ ]
699- const relocateHrefs = relocatePlaceholders . map ( ( e ) =>
700- e . getAttribute ( 'data-n-staging' )
654+ const desiredHrefs = new Set ( styleSheets . map ( ( s ) => s . href ) )
655+ const currentStyleTags = looseToArray < HTMLStyleElement > (
656+ document . querySelectorAll ( 'style[data-n-href]' )
701657 )
702- ; ( [ ] . slice . call (
703- document . querySelectorAll ( 'link[ data-n-p]' )
704- ) as HTMLLinkElement [ ] ) . forEach ( ( el ) => {
705- const currentHref = el . getAttribute ( 'href' )
706- const relocateIndex = relocateHrefs . indexOf ( currentHref )
707- if ( relocateIndex !== - 1 ) {
708- const placeholderElement = relocatePlaceholders [ relocateIndex ]
709- placeholderElement . parentNode ?. replaceChild ( el , placeholderElement )
658+ const currentHrefs = currentStyleTags . map (
659+ ( tag ) => tag . getAttribute ( ' data-n-href' ) !
660+ )
661+
662+ // Toggle `<style>` tags on or off depending on if they're needed:
663+ for ( let idx = 0 ; idx < currentHrefs . length ; ++ idx ) {
664+ if ( desiredHrefs . has ( currentHrefs [ idx ] ) ) {
665+ currentStyleTags [ idx ] . removeAttribute ( 'media' )
710666 } else {
711- el . parentNode ! . removeChild ( el )
667+ currentStyleTags [ idx ] . setAttribute ( 'media' , 'x' )
712668 }
713- } )
669+ }
714670
715- // Activate new stylesheets:
716- ; [ ] . slice
717- . call ( document . querySelectorAll ( 'link[data-n-staging]' ) )
718- . forEach ( ( el : HTMLLinkElement ) => {
719- el . removeAttribute ( 'data-n-staging' )
720- el . removeAttribute ( 'media' )
721- el . setAttribute ( 'data-n-p' , '' )
671+ // Reorder styles into intended order:
672+ let referenceNode = document . querySelector ( 'noscript[data-n-css]' )
673+ if (
674+ // This should be an invariant:
675+ referenceNode
676+ ) {
677+ styleSheets . forEach ( ( { href } ) => {
678+ const targetTag = document . querySelector (
679+ `style[data-n-href="${ href } "]`
680+ )
681+ if (
682+ // This should be an invariant:
683+ targetTag
684+ ) {
685+ referenceNode ! . parentNode ! . insertBefore (
686+ targetTag ,
687+ referenceNode ! . nextSibling
688+ )
689+ referenceNode = targetTag
690+ }
722691 } )
692+ }
693+
694+ // Finally, clean up server rendered stylesheets:
695+ looseToArray < HTMLLinkElement > (
696+ document . querySelectorAll ( 'link[data-n-p]' )
697+ ) . forEach ( ( el ) => {
698+ el . parentNode ! . removeChild ( el )
699+ } )
723700
724- // Force browser to recompute layout, which prevents a flash of unstyled
725- // content:
701+ // Force browser to recompute layout, which should prevent a flash of
702+ // unstyled content:
726703 getComputedStyle ( document . body , 'height' )
727704 }
728705
@@ -737,33 +714,19 @@ function doRender({
737714 </ Root >
738715 )
739716
717+ onStart ( )
718+
740719 // We catch runtime errors using componentDidCatch which will trigger renderError
741- return Promise . race ( [
742- // Download required CSS assets first:
743- onStart ( )
744- . then ( ( ) => {
745- // Ensure a new render has not been started:
746- if ( renderPromiseReject === lastRenderReject ) {
747- // Queue rendering:
748- renderReactElement (
749- process . env . __NEXT_STRICT_MODE ? (
750- < React . StrictMode > { elem } </ React . StrictMode >
751- ) : (
752- elem
753- ) ,
754- appElement !
755- )
756- }
757- } )
758- . then (
759- ( ) =>
760- // Wait for rendering to complete:
761- renderPromise
762- ) ,
763-
764- // Bail early on route cancelation (rejection):
765- renderPromise ,
766- ] )
720+ renderReactElement (
721+ process . env . __NEXT_STRICT_MODE ? (
722+ < React . StrictMode > { elem } </ React . StrictMode >
723+ ) : (
724+ elem
725+ ) ,
726+ appElement !
727+ )
728+
729+ return renderPromise
767730}
768731
769732function Root ( {
0 commit comments