|
| 1 | +/** |
| 2 | + * Note: this file gets built separately from the rest of the core module, and is then kept separate |
| 3 | + * in the dist directory via manualChunks. This way it can run before the rest of the core module is |
| 4 | + * loaded, but core can still use it. |
| 5 | + * |
| 6 | + * Here we handle preloading of chunks. |
| 7 | + * |
| 8 | + * Given a symbol hash (in fact any string), we can find all the chunks that it depends on, via the |
| 9 | + * bundle graph. We then generate preload link tags for each of those chunks. |
| 10 | + * |
| 11 | + * The priority is set to high for direct imports and low for indirect imports. |
| 12 | + * |
| 13 | + * There are several parts to this: |
| 14 | + * |
| 15 | + * - Load the bundle graph from the preload link tag that was injected during SSR |
| 16 | + * - Given a string, find all the chunks that it depends on |
| 17 | + * - Generate the preload link tags if needed |
| 18 | + */ |
| 19 | +/** |
| 20 | + * Todo |
| 21 | + * |
| 22 | + * - High and low priority sets |
| 23 | + * - Num preloads active at a time |
| 24 | + * - If no modulepreload support, do 1 fetch at a time |
| 25 | + */ |
| 26 | + |
| 27 | +import { isBrowser } from '@builder.io/qwik/build'; |
| 28 | +import type { QwikBundleGraph } from '../optimizer/src/types'; |
| 29 | + |
| 30 | +const enum BundleImportState { |
| 31 | + None, |
| 32 | + Low, |
| 33 | + Queued, |
| 34 | + Loading, |
| 35 | +} |
| 36 | +type BundleImport = { |
| 37 | + $url$: string | null; |
| 38 | + $state$: BundleImportState; |
| 39 | + $imports$: string[]; |
| 40 | + $dynamicImports$: string[]; |
| 41 | +}; |
| 42 | +const bundles = new Map<string, BundleImport>(); |
| 43 | +const high: BundleImport[] = []; |
| 44 | +const low: BundleImport[] = []; |
| 45 | + |
| 46 | +let base: string | undefined; |
| 47 | + |
| 48 | +// minification helpers |
| 49 | +const doc = isBrowser ? document : undefined!; |
| 50 | +const modulePreloadStr = 'modulepreload'; |
| 51 | +const preloadStr = 'preload'; |
| 52 | + |
| 53 | +const makeBundle = (path: string, imports: string[], dynamicImports: string[]) => { |
| 54 | + const url = path.endsWith('.js') ? new URL(`${base}${path}`, doc.baseURI).toString() : null; |
| 55 | + return { |
| 56 | + $url$: url, |
| 57 | + $state$: BundleImportState.None, |
| 58 | + $imports$: imports, |
| 59 | + $dynamicImports$: dynamicImports, |
| 60 | + }; |
| 61 | +}; |
| 62 | + |
| 63 | +/** |
| 64 | + * Lazily load the bundle graph and then import dependencies of bundles that were loaded already. |
| 65 | + * |
| 66 | + * @internal |
| 67 | + */ |
| 68 | +const loadBundleGraph = (basePath: string, manifestHash: string) => { |
| 69 | + if (!isBrowser || base) { |
| 70 | + return; |
| 71 | + } |
| 72 | + base = basePath; |
| 73 | + // TODO check TTI, maybe inject fetch link with timeout so we don't do the fetch directly |
| 74 | + fetch(`${basePath}q-bundle-graph-${manifestHash}.json`) |
| 75 | + .then((res) => res.text()) |
| 76 | + .then((text) => parseBundleGraph(text, basePath)) |
| 77 | + .catch(console.error); |
| 78 | +}; |
| 79 | + |
| 80 | +// TODO fallback to fetch instead of preload? |
| 81 | +const rel = |
| 82 | + isBrowser && doc.createElement('link').relList.supports(modulePreloadStr) |
| 83 | + ? modulePreloadStr |
| 84 | + : preloadStr; |
| 85 | + |
| 86 | +let highCount = 0; |
| 87 | +let lowCount = 0; |
| 88 | +const trigger = () => { |
| 89 | + while (highCount < 4 && high.length) { |
| 90 | + const bundle = high.pop()!; |
| 91 | + makePreloadLink(bundle!, true); |
| 92 | + } |
| 93 | + while (lowCount < 2 && low.length) { |
| 94 | + const bundle = low.pop()!; |
| 95 | + makePreloadLink(bundle!); |
| 96 | + } |
| 97 | +}; |
| 98 | + |
| 99 | +const makePreloadLink = (bundle: BundleImport, priority?: boolean) => { |
| 100 | + if (bundle.$state$ === BundleImportState.Loading) { |
| 101 | + return; |
| 102 | + } |
| 103 | + bundle.$state$ = BundleImportState.Loading; |
| 104 | + const link = doc.createElement('link'); |
| 105 | + link.href = bundle.$url$!; |
| 106 | + if (priority) { |
| 107 | + highCount++; |
| 108 | + link.rel = rel; |
| 109 | + } else { |
| 110 | + lowCount++; |
| 111 | + link.rel = preloadStr; |
| 112 | + link.fetchPriority = 'low'; |
| 113 | + } |
| 114 | + link.as = 'script'; |
| 115 | + link.onload = link.onerror = () => { |
| 116 | + link.remove(); |
| 117 | + if (priority) { |
| 118 | + highCount--; |
| 119 | + } else { |
| 120 | + lowCount--; |
| 121 | + } |
| 122 | + trigger(); |
| 123 | + }; |
| 124 | + |
| 125 | + doc.head.appendChild(link); |
| 126 | + return 1; |
| 127 | +}; |
| 128 | + |
| 129 | +const preloadBundle = (bundle: BundleImport, priority: boolean) => { |
| 130 | + if (bundle.$state$ > BundleImportState.Queued) { |
| 131 | + return; |
| 132 | + } |
| 133 | + if (bundle.$url$) { |
| 134 | + (priority ? high : low).unshift(bundle); |
| 135 | + } |
| 136 | + bundle.$state$ = priority ? BundleImportState.Queued : BundleImportState.Low; |
| 137 | +}; |
| 138 | + |
| 139 | +/** |
| 140 | + * Preload a bundle or bundles. Requires calling loadBundleGraph first. |
| 141 | + * |
| 142 | + * @internal |
| 143 | + */ |
| 144 | +const preload = (name: string | string[], priority: boolean, seen?: Set<BundleImport>) => { |
| 145 | + if (!isBrowser || !base) { |
| 146 | + return; |
| 147 | + } |
| 148 | + if (Array.isArray(name)) { |
| 149 | + name.forEach((n) => preload(n, priority)); |
| 150 | + return; |
| 151 | + } |
| 152 | + let bundle = bundles.get(name); |
| 153 | + if (!bundle) { |
| 154 | + bundle = makeBundle(name, [], []); |
| 155 | + bundles.set(name, bundle); |
| 156 | + } |
| 157 | + if (seen && seen.has(bundle)) { |
| 158 | + // prevent loops |
| 159 | + return; |
| 160 | + } |
| 161 | + seen?.add(bundle); |
| 162 | + preloadBundle(bundle, priority); |
| 163 | + for (const importName of bundle.$imports$) { |
| 164 | + preload(importName, priority, seen || new Set([bundle])); |
| 165 | + } |
| 166 | + for (const importName of bundle.$dynamicImports$) { |
| 167 | + preload(importName, false, seen || new Set([bundle])); |
| 168 | + } |
| 169 | + trigger(); |
| 170 | +}; |
| 171 | + |
| 172 | +const parseBundleGraph = (text: string, base: string) => { |
| 173 | + try { |
| 174 | + const graph = JSON.parse(text) as QwikBundleGraph; |
| 175 | + let i = 0; |
| 176 | + const toProcess = []; |
| 177 | + while (i < graph.length) { |
| 178 | + const name = graph[i++] as string; |
| 179 | + const imports: string[] = []; |
| 180 | + const dynamicImports: string[] = []; |
| 181 | + let idx: number | string; |
| 182 | + let collection = imports; |
| 183 | + while (((idx = graph[i]), typeof idx === 'number')) { |
| 184 | + if (idx === -1) { |
| 185 | + collection = dynamicImports; |
| 186 | + } else { |
| 187 | + collection.push(graph[idx] as string); |
| 188 | + } |
| 189 | + i++; |
| 190 | + } |
| 191 | + if (bundles.has(name)) { |
| 192 | + const bundle = bundles.get(name)!; |
| 193 | + bundle.$imports$ = imports; |
| 194 | + bundle.$dynamicImports$ = dynamicImports; |
| 195 | + toProcess.push(bundle); |
| 196 | + } else { |
| 197 | + bundles.set(name, makeBundle(name, imports, dynamicImports)); |
| 198 | + } |
| 199 | + } |
| 200 | + for (const bundle of toProcess) { |
| 201 | + for (const importName of bundle.$imports$) { |
| 202 | + preload(importName, bundle.$state$ !== BundleImportState.Queued); |
| 203 | + } |
| 204 | + for (const importName of bundle.$dynamicImports$) { |
| 205 | + preload(importName, false); |
| 206 | + } |
| 207 | + } |
| 208 | + } catch (e) { |
| 209 | + console.error('Error parsing bundle graph', e, text); |
| 210 | + throw e; |
| 211 | + } |
| 212 | +}; |
| 213 | + |
| 214 | +// Short names for minification |
| 215 | +export { loadBundleGraph as l, preload as p }; |
0 commit comments