@@ -17,7 +17,7 @@ import * as envConfig from '../next-server/lib/runtime-config'
17
17
import { getURL , loadGetInitialProps , ST } from '../next-server/lib/utils'
18
18
import type { NEXT_DATA } from '../next-server/lib/utils'
19
19
import initHeadManager from './head-manager'
20
- import PageLoader , { createLink } from './page-loader'
20
+ import PageLoader , { StyleSheetTuple } from './page-loader'
21
21
import measureWebVitals from './performance-relayer'
22
22
import { createRouter , makePublicRouterInstance } from './router'
23
23
@@ -84,13 +84,25 @@ if (hasBasePath(asPath)) {
84
84
85
85
type RegisterFn = ( input : [ string , ( ) => void ] ) => void
86
86
87
+ const looseToArray = < T extends { } > ( input : any ) : T [ ] => [ ] . slice . call ( input )
88
+
87
89
const pageLoader = new PageLoader (
88
90
buildId ,
89
91
prefix ,
90
92
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
+ } ) )
94
106
)
95
107
const register : RegisterFn = ( [ r , f ] ) => pageLoader . registerPage ( r , f )
96
108
if ( window . __NEXT_P ) {
@@ -109,7 +121,7 @@ let lastRenderReject: (() => void) | null
109
121
let webpackHMR : any
110
122
export let router : Router
111
123
let CachedComponent : React . ComponentType
112
- let cachedStyleSheets : string [ ]
124
+ let cachedStyleSheets : StyleSheetTuple [ ]
113
125
let CachedApp : AppComponent , onPerfEntry : ( metric : any ) => void
114
126
115
127
class Container extends React . Component < {
@@ -574,8 +586,8 @@ function doRender({
574
586
// lastAppProps has to be set before ReactDom.render to account for ReactDom throwing an error.
575
587
lastAppProps = appProps
576
588
589
+ let canceled = false
577
590
let resolvePromise : ( ) => void
578
- let renderPromiseReject : ( ) => void
579
591
const renderPromise = new Promise ( ( resolve , reject ) => {
580
592
if ( lastRenderReject ) {
581
593
lastRenderReject ( )
@@ -584,7 +596,8 @@ function doRender({
584
596
lastRenderReject = null
585
597
resolve ( )
586
598
}
587
- renderPromiseReject = lastRenderReject = ( ) => {
599
+ lastRenderReject = ( ) => {
600
+ canceled = true
588
601
lastRenderReject = null
589
602
590
603
const error : any = new Error ( 'Cancel rendering route' )
@@ -593,12 +606,9 @@ function doRender({
593
606
}
594
607
} )
595
608
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 {
602
612
if (
603
613
// We can skip this during hydration. Running it wont cause any harm, but
604
614
// we may as well save the CPU cycles.
@@ -607,78 +617,27 @@ function doRender({
607
617
// unless we're in production:
608
618
process . env . NODE_ENV !== 'production'
609
619
) {
610
- return Promise . resolve ( [ ] )
620
+ return false
611
621
}
612
622
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' ) )
627
628
)
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
- }
649
629
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 ) )
658
638
}
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 ( ( ) => { } )
681
639
} )
640
+ return true
682
641
}
683
642
684
643
function onCommit ( ) {
@@ -689,40 +648,58 @@ function doRender({
689
648
// We can skip this during hydration. Running it wont cause any harm, but
690
649
// we may as well save the CPU cycles:
691
650
! isInitialRender &&
692
- // Ensure this render commit owns the currently staged stylesheets:
693
- renderPromiseReject === lastRenderReject
651
+ // Ensure this render was not canceled
652
+ ! canceled
694
653
) {
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]' )
701
657
)
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' )
710
666
} else {
711
- el . parentNode ! . removeChild ( el )
667
+ currentStyleTags [ idx ] . setAttribute ( 'media' , 'x' )
712
668
}
713
- } )
669
+ }
714
670
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
+ }
722
691
} )
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
+ } )
723
700
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:
726
703
getComputedStyle ( document . body , 'height' )
727
704
}
728
705
@@ -737,33 +714,19 @@ function doRender({
737
714
</ Root >
738
715
)
739
716
717
+ onStart ( )
718
+
740
719
// 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
767
730
}
768
731
769
732
function Root ( {
0 commit comments