|
| 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 bundles. |
| 7 | + * |
| 8 | + * Given a symbol hash (in fact any string), we can find all the bundles that it depends on, via the |
| 9 | + * bundle graph. We then generate preload link tags for each of those bundles. |
| 10 | + * |
| 11 | + * There are several parts to this: |
| 12 | + * |
| 13 | + * - Load the bundle graph from the preload link tag that was injected during SSR |
| 14 | + * - Given a string, find all the bundles that it depends on |
| 15 | + * - Generate the preload link tags if needed |
| 16 | + * |
| 17 | + * In practice, we queue incoming requests and when we process |
| 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 | +let highCount = 0; |
| 54 | +let lowCount = 0; |
| 55 | +/** |
| 56 | + * This is called when a bundle is queued or finished loading. |
| 57 | + * |
| 58 | + * Because Chrome doesn't treat new modulepreloads as higher priority, we only make 5 links |
| 59 | + * available at a time, so that when a new high priority bundle comes in, it is soon preloaded. |
| 60 | + * |
| 61 | + * We make sure to first empty the high priority items, first-in-last-out. |
| 62 | + */ |
| 63 | +const trigger = () => { |
| 64 | + while (highCount < 6 && high.length) { |
| 65 | + const bundle = high.pop()!; |
| 66 | + preloadOne(bundle!, true); |
| 67 | + } |
| 68 | + while (highCount + lowCount < 6 && low.length) { |
| 69 | + const bundle = low.pop()!; |
| 70 | + preloadOne(bundle!); |
| 71 | + } |
| 72 | +}; |
| 73 | + |
| 74 | +const rel = |
| 75 | + isBrowser && doc.createElement('link').relList.supports(modulePreloadStr) |
| 76 | + ? modulePreloadStr |
| 77 | + : preloadStr; |
| 78 | +/** |
| 79 | + * Note, we considered using `preload` for low priority bundles, but those don't get preparsed and |
| 80 | + * that slows down interaction |
| 81 | + */ |
| 82 | +const preloadOne = (bundle: BundleImport, priority?: boolean) => { |
| 83 | + if (bundle.$state$ === BundleImportState.Loading) { |
| 84 | + return; |
| 85 | + } |
| 86 | + if (bundle.$url$) { |
| 87 | + const link = doc.createElement('link'); |
| 88 | + link.href = bundle.$url$!; |
| 89 | + link.rel = rel; |
| 90 | + if (priority) { |
| 91 | + highCount++; |
| 92 | + } else { |
| 93 | + lowCount++; |
| 94 | + } |
| 95 | + link.as = 'script'; |
| 96 | + link.onload = link.onerror = () => { |
| 97 | + link.remove(); |
| 98 | + if (priority) { |
| 99 | + highCount--; |
| 100 | + } else { |
| 101 | + lowCount--; |
| 102 | + } |
| 103 | + trigger(); |
| 104 | + }; |
| 105 | + |
| 106 | + doc.head.appendChild(link); |
| 107 | + } |
| 108 | + |
| 109 | + bundle.$state$ = BundleImportState.Loading; |
| 110 | + /** Now that we processed the bundle, its dependencies are needed ASAP */ |
| 111 | + preload(bundle.$dynamicImports$); |
| 112 | + preload(bundle.$imports$, priority); |
| 113 | +}; |
| 114 | + |
| 115 | +const makeBundle = (path: string, imports: string[], dynamicImports: string[]) => { |
| 116 | + const url = path.endsWith('.js') ? new URL(`${base}${path}`, doc.baseURI).toString() : null; |
| 117 | + return { |
| 118 | + $url$: url, |
| 119 | + $state$: BundleImportState.None, |
| 120 | + $imports$: imports, |
| 121 | + $dynamicImports$: dynamicImports, |
| 122 | + }; |
| 123 | +}; |
| 124 | +const ensureBundle = (name: string) => { |
| 125 | + let bundle = bundles.get(name); |
| 126 | + if (!bundle) { |
| 127 | + bundle = makeBundle(name, [], []); |
| 128 | + bundles.set(name, bundle); |
| 129 | + } |
| 130 | + if (bundle.$state$ === BundleImportState.Loading) { |
| 131 | + return; |
| 132 | + } |
| 133 | + return bundle; |
| 134 | +}; |
| 135 | + |
| 136 | +const parseBundleGraph = (text: string, base: string) => { |
| 137 | + const graph = JSON.parse(text) as QwikBundleGraph; |
| 138 | + let i = 0; |
| 139 | + // All existing loading bundles need imports processed |
| 140 | + const toProcess = Object.keys(bundles) |
| 141 | + .filter((name) => bundles.get(name)!.$state$ === BundleImportState.Loading) |
| 142 | + .reverse(); |
| 143 | + while (i < graph.length) { |
| 144 | + const name = graph[i++] as string; |
| 145 | + const imports: string[] = []; |
| 146 | + const dynamicImports: string[] = []; |
| 147 | + let idx: number | string; |
| 148 | + let collection = imports; |
| 149 | + while (((idx = graph[i]), typeof idx === 'number')) { |
| 150 | + if (idx === -1) { |
| 151 | + collection = dynamicImports; |
| 152 | + } else { |
| 153 | + collection.push(graph[idx] as string); |
| 154 | + } |
| 155 | + i++; |
| 156 | + } |
| 157 | + if (bundles.has(name)) { |
| 158 | + const bundle = bundles.get(name)!; |
| 159 | + bundle.$imports$ = imports; |
| 160 | + bundle.$dynamicImports$ = dynamicImports; |
| 161 | + } else { |
| 162 | + bundles.set(name, makeBundle(name, imports, dynamicImports)); |
| 163 | + } |
| 164 | + } |
| 165 | + for (const name of toProcess) { |
| 166 | + const bundle = bundles.get(name)!; |
| 167 | + // we assume low priority |
| 168 | + preload(bundle.$dynamicImports$); |
| 169 | + preload(bundle.$imports$); |
| 170 | + } |
| 171 | +}; |
| 172 | + |
| 173 | +/** |
| 174 | + * Preload a bundle or bundles. Requires calling loadBundleGraph first. |
| 175 | + * |
| 176 | + * @internal |
| 177 | + */ |
| 178 | +const preload = (name: string | string[], priority?: boolean) => { |
| 179 | + if (!isBrowser || !base) { |
| 180 | + return; |
| 181 | + } |
| 182 | + const queue = priority ? high : low; |
| 183 | + if (Array.isArray(name)) { |
| 184 | + queue.push(...(name.map(ensureBundle).filter(Boolean) as BundleImport[]).reverse()); |
| 185 | + } else { |
| 186 | + const bundle = ensureBundle(name); |
| 187 | + if (bundle) { |
| 188 | + queue.push(bundle); |
| 189 | + } |
| 190 | + } |
| 191 | + trigger(); |
| 192 | +}; |
| 193 | + |
| 194 | +/** |
| 195 | + * Lazily load the bundle graph and then import dependencies of bundles that were loaded already. |
| 196 | + * |
| 197 | + * @internal |
| 198 | + */ |
| 199 | +const loadBundleGraph = (basePath: string, manifestHash: string) => { |
| 200 | + if (!isBrowser || base) { |
| 201 | + return; |
| 202 | + } |
| 203 | + base = basePath; |
| 204 | + // TODO check TTI, maybe inject fetch link with timeout so we don't do the fetch directly |
| 205 | + fetch(`${basePath}q-bundle-graph-${manifestHash}.json`) |
| 206 | + .then((res) => res.text()) |
| 207 | + .then((text) => parseBundleGraph(text, basePath)) |
| 208 | + // We warn because it's not critical, and in the CI tests Windows serves up a HTML file instead the bundle graph sometimes, which breaks the tests that don't expect error logs |
| 209 | + .catch(console.warn); |
| 210 | +}; |
| 211 | + |
| 212 | +// Short names for minification |
| 213 | +export { loadBundleGraph as l, preload as p }; |
0 commit comments