1- import { createRenderEffect , createResource , JSX , onCleanup , sharedConfig } from "solid-js" ;
1+ import { createRenderEffect , createResource , onCleanup , sharedConfig } from "solid-js" ;
22import { getRequestEvent , isServer , useAssets as useAssets_ } from "solid-js/web" ;
33import { renderAsset , type Asset } from "../server/renderAsset.tsx" ;
44
55/**
66 * Keep's Solid suspended while loading CSS
7- * Prevents FOUC's
7+ * Prevents FOUC's for uncached CSS
88 */
99const EXPERIMENTAL_CSS_SUSPENSE = true ;
1010
@@ -19,7 +19,13 @@ const CANCEL_EVENT = "cancel";
1919const EVENT_REGISTRY = Symbol ( "assetRegistry" ) ;
2020const NOOP = ( ) => "" ;
2121
22- type AssetEntity = { key : string ; consumers : number ; el ?: HTMLElement ; ssrIdx ?: number } ;
22+ type AssetEntity = {
23+ key : string ;
24+ consumers : number ;
25+ el ?: HTMLElement ;
26+ preloadEl ?: HTMLLinkElement ;
27+ ssrIdx ?: number ;
28+ } ;
2329type Registry = Record < string , AssetEntity > ;
2430type AttrKeys = keyof Asset [ "attrs" ] ;
2531
@@ -53,7 +59,7 @@ export const useAssets = (assets: Asset[], nonce?: string) => {
5359 const cssKeys : string [ ] = [ ] ;
5460
5561 const cssSuspense = EXPERIMENTAL_CSS_SUSPENSE && ! sharedConfig . context ;
56- const cssPromises : Promise < any > [ ] = [ ] ;
62+ const preloadPromises : Promise < { placeholderEl : Text ; entity : AssetEntity } > [ ] = [ ] ;
5763
5864 for ( const asset of assets ) {
5965 const entity = getEntity ( registry , asset ) ;
@@ -72,40 +78,52 @@ export const useAssets = (assets: Asset[], nonce?: string) => {
7278 entity . ssrIdx = ssrRequestAssets . length - 1 ;
7379 } else {
7480 const el = ( entity . el = document . createElement ( asset . tag ) ) ;
75-
76- if ( cssSuspense && isCSSLink ) {
77- cssPromises . push (
78- new Promise ( res => {
79- el . addEventListener ( "load" , res , { once : true } ) ;
80- el . addEventListener ( CANCEL_EVENT , res , { once : true } ) ;
81- el . addEventListener ( "error" , res , { once : true } ) ;
82- } )
83- ) ;
84- }
85-
8681 if ( EXPERIMENTAL_BLOCKING && isCSS ) {
8782 el . setAttribute ( "blocking" , "render" ) ;
8883 }
89-
9084 for ( const k of Object . keys ( asset . attrs ) ) {
9185 if ( k === "key" ) continue ;
9286 el . setAttribute ( k , asset . attrs [ k as AttrKeys ] ) ;
9387 }
94-
9588 if ( "children" in asset ) {
9689 el . innerHTML = asset . children as string ;
9790 }
9891
99- document . head . appendChild ( el ) ;
92+ if ( cssSuspense && isCSS ) {
93+ const placeholderEl = document . createTextNode ( "" ) ;
94+ document . head . appendChild ( placeholderEl ) ;
95+
96+ preloadPromises . push (
97+ ( async ( ) => {
98+ if ( isCSSLink && asset . attrs . href ) {
99+ const preloadEl = ( entity . preloadEl = preloadStyle ( asset . attrs . href ) ) ;
100+ await new Promise ( res => {
101+ [ "load" , "error" , CANCEL_EVENT ] . map ( t => preloadEl . addEventListener ( t , res ) ) ;
102+ } ) ;
103+ }
104+
105+ return { placeholderEl, entity } ;
106+ } ) ( )
107+ ) ;
108+ } else {
109+ document . head . appendChild ( el ) ;
110+ }
100111 }
101112 }
102113
103- if ( cssSuspense && cssPromises . length ) {
104- const [ r ] = createResource ( ( ) => Promise . all ( cssPromises ) ) ;
105- createRenderEffect ( r ) ;
114+ if ( cssSuspense && preloadPromises . length ) {
115+ const [ r ] = createResource ( ( ) => Promise . all ( preloadPromises ) ) ;
116+ createRenderEffect ( ( ) => {
117+ const preloads = r ( ) ;
118+ if ( ! preloads ) return ;
119+
120+ for ( const { entity, placeholderEl } of preloads ) {
121+ if ( ! entity . consumers || ! entity . el ) continue ;
122+ placeholderEl . replaceWith ( entity . el ) ;
123+ }
124+ } ) ;
106125 }
107126
108- // Unmounting logic
109127 onCleanup ( ( ) => {
110128 for ( const key of cssKeys ) {
111129 const entity = registry [ key ] ! ;
@@ -118,28 +136,25 @@ export const useAssets = (assets: Asset[], nonce?: string) => {
118136 // Ideally this logic should be implemented directly in dom-expressions
119137 ssrRequestAssets . splice ( entity . ssrIdx ! , 1 , NOOP ) ;
120138 } else {
121- if ( EXPERIMENTAL_CSS_SUSPENSE ) entity . el ! . dispatchEvent ( new CustomEvent ( CANCEL_EVENT ) ) ;
139+ if ( entity . preloadEl ) {
140+ entity . preloadEl . dispatchEvent ( new CustomEvent ( CANCEL_EVENT ) ) ;
141+ entity . preloadEl . remove ( ) ;
142+ }
122143 entity . el ! . remove ( ) ;
123144 delete registry [ key ] ;
124145 }
125146 }
126147 } ) ;
127148} ;
128149
129- export const preloadStyles = ( assets : Asset [ ] ) => {
130- if ( import . meta. env . SSR ) return ;
131- for ( const asset of assets ) {
132- const attrs = asset . attrs as JSX . LinkHTMLAttributes < HTMLLinkElement > ;
133- if ( ! attrs . href || attrs . rel !== "stylesheet" ) return ;
134-
135- let element = document . head . querySelector ( `link[href="${ attrs . href } "]` ) ;
136- if ( element ) return ;
137-
138- // create a link preload element for the css file so it starts loading but doesnt get attached
139- element = document . createElement ( "link" ) ;
140- element . setAttribute ( "rel" , "preload" ) ;
141- element . setAttribute ( "as" , "style" ) ;
142- element . setAttribute ( "href" , attrs . href ) ;
143- document . head . appendChild ( element ) ;
144- }
150+ const preloadStyle = ( href : string ) => {
151+ const element = document . createElement ( "link" ) ;
152+
153+ element . setAttribute ( "rel" , "preload" ) ;
154+ element . setAttribute ( "as" , "style" ) ;
155+ element . setAttribute ( "href" , href ) ;
156+
157+ document . head . appendChild ( element ) ;
158+
159+ return element ;
145160} ;
0 commit comments