Skip to content

Commit 978933f

Browse files
committed
fix: prevent frames of pre-styled content with EXPERIMENTAL_CSS_SUSPENSE
1 parent a7bd035 commit 978933f

File tree

2 files changed

+55
-42
lines changed

2 files changed

+55
-42
lines changed

packages/start/src/shared/assets.ts

Lines changed: 54 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { createRenderEffect, createResource, JSX, onCleanup, sharedConfig } from "solid-js";
1+
import { createRenderEffect, createResource, onCleanup, sharedConfig } from "solid-js";
22
import { getRequestEvent, isServer, useAssets as useAssets_ } from "solid-js/web";
33
import { 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
*/
99
const EXPERIMENTAL_CSS_SUSPENSE = true;
1010

@@ -19,7 +19,13 @@ const CANCEL_EVENT = "cancel";
1919
const EVENT_REGISTRY = Symbol("assetRegistry");
2020
const 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+
};
2329
type Registry = Record<string, AssetEntity>;
2430
type 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
};

packages/start/src/shared/lazy.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Component, createComponent, lazy as solidLazy } from "solid-js";
22
import { getRequestEvent } from "solid-js/web";
33
import { getManifest } from "solid-start:get-manifest";
44
import { Asset } from "../server/types.ts";
5-
import { preloadStyles, useAssets } from "./assets.ts";
5+
import { useAssets } from "./assets.ts";
66

77
const assetsById: Record<string, Asset[]> = {};
88

@@ -32,8 +32,6 @@ const collectAssets = function <T extends () => Promise<{ default: Component<any
3232
const assets: Asset[] = await getAssets(id);
3333
if (!assets.length) return mod;
3434

35-
preloadStyles(assets);
36-
3735
return {
3836
default: (props: any) => {
3937
// TODO: How to get nonce on the client

0 commit comments

Comments
 (0)