Skip to content

Commit

Permalink
feat: reduce prefetch request waterfall (QwikDev#1204)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamdbradley authored Sep 4, 2022
1 parent 7e0b522 commit 49250a1
Show file tree
Hide file tree
Showing 11 changed files with 267 additions and 116 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { BuildContext } from '../types';
import swRegister from '@qwik-city-sw-register-build';
import type { QwikManifest } from '@builder.io/qwik/optimizer';
import type { AppBundles } from '../../runtime/src/library/service-worker/types';
import type { AppBundle } from '../../runtime/src/library/service-worker/types';
import { removeExtension } from '../../utils/fs';

export function generateServiceWorkerRegister(ctx: BuildContext) {
let swReg: string;
Expand All @@ -25,42 +26,122 @@ export function generateServiceWorkerRegister(ctx: BuildContext) {
return `export default ${JSON.stringify(swReg)};`;
}

export function prependManifestToServiceWorker(manifest: QwikManifest, swCode: string) {
export function prependManifestToServiceWorker(
ctx: BuildContext,
manifest: QwikManifest,
swCode: string
) {
const key = `/* Qwik Service Worker */`;
if (swCode.includes(key)) {
// both SSR and SSG could have ran this code,
// just check if we already prepended the bundles
return null;
}

const appBundlesCode = generateAppBundles(manifest);
const appBundles: AppBundle[] = [];
const appBundlesCode = generateAppBundles(appBundles, manifest);
const libraryBundlesCode = generateLibraryBundles(appBundles, manifest);
const linkBundlesCode = generateLinkBundles(ctx, appBundles, manifest);

return [key, appBundlesCode, swCode].join('\n');
return [key, appBundlesCode, libraryBundlesCode, linkBundlesCode, swCode].join('\n');
}

function generateAppBundles(manifest: QwikManifest) {
const appBundles: AppBundles = {};

function generateAppBundles(appBundles: AppBundle[], manifest: QwikManifest) {
for (const appBundleName in manifest.bundles) {
appBundles.push([appBundleName, []]);
}

for (const appBundle of appBundles) {
const appBundleName = appBundle[0];
const importedBundleIds = appBundle[1];
const symbolHashesInBundle: string[] = [];

const manifestBundle = manifest.bundles[appBundleName];
const importedBundleNames = Array.isArray(manifestBundle.imports) ? manifestBundle.imports : [];
const symbolHashesInBundle = new Set<string>();
for (const importedBundleName of importedBundleNames) {
importedBundleIds.push(getAppBundleId(appBundles, importedBundleName));
}

if (manifestBundle.symbols) {
for (const manifestBundleSymbolName of manifestBundle.symbols) {
const symbol = manifest.symbols[manifestBundleSymbolName];
if (symbol?.hash) {
symbolHashesInBundle.add(symbol.hash);
if (symbol?.hash && !symbolHashesInBundle.includes(symbol.hash)) {
symbolHashesInBundle.push(symbol.hash);
}
}
}

appBundles[appBundleName] = [importedBundleNames, Array.from(symbolHashesInBundle)];
if (symbolHashesInBundle.length > 0) {
appBundle[2] = symbolHashesInBundle;
}
}

return `const appBundles=${JSON.stringify(appBundles)};`;
}

function generateLibraryBundles(appBundles: AppBundle[], manifest: QwikManifest) {
const libraryBundleIds: number[] = [];

for (const [bundleName, bundle] of Object.entries(manifest.bundles)) {
if (bundle.origins && bundle.origins.includes('@qwik-city-plan')) {
libraryBundleIds.push(getAppBundleId(appBundles, bundleName));
break;
}
}

return `const libraryBundleIds=${JSON.stringify(libraryBundleIds)};`;
}

function generateLinkBundles(ctx: BuildContext, appBundles: AppBundle[], manifest: QwikManifest) {
const linkBundles: string[] = [];

for (const r of ctx.routes) {
const linkBundleNames: string[] = [];

const addFileBundles = (filePath: string) => {
for (const [bundleName, bundle] of Object.entries(manifest.bundles)) {
if (bundle.origins) {
for (const bundleOrigin of bundle.origins) {
const srcPath = removeExtension(filePath);
const bundleOrginPath = removeExtension(bundleOrigin);

if (srcPath.endsWith(bundleOrginPath)) {
if (!linkBundleNames.includes(bundleName)) {
linkBundleNames.push(bundleName);
}

if (bundle.dynamicImports) {
for (const dynamicImport of bundle.dynamicImports) {
if (!linkBundleNames.includes(dynamicImport)) {
linkBundleNames.push(dynamicImport);
}
}
}
}
}
}
}
};

for (const layout of r.layouts) {
addFileBundles(layout.filePath);
}
addFileBundles(r.filePath);

linkBundles.push(
`[${r.pattern.toString()},${JSON.stringify(
linkBundleNames.map((bundleName) => getAppBundleId(appBundles, bundleName))
)}]`
);
}

return `const linkBundles=[${linkBundles.join(',')}];`;
}

function getAppBundleId(appBundles: AppBundle[], bundleName: string) {
return appBundles.findIndex((b) => b[0] === bundleName);
}

const SW_UNREGISTER = `
navigator.serviceWorker.getRegistrations().then((regs) => {
for (const reg of regs) {
Expand Down
2 changes: 1 addition & 1 deletion packages/qwik-city/buildtime/vite/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export function qwikCity(userOpts?: QwikCityVitePluginOptions) {
const swClientDistPath = join(clientOutDir, swEntry.chunkFileName);

const swCode = await readFile(swClientDistPath, 'utf-8');
const swCodeUpdate = prependManifestToServiceWorker(manifest, swCode);
const swCodeUpdate = prependManifestToServiceWorker(ctx, manifest, swCode);
if (swCodeUpdate) {
await writeFile(swClientDistPath, swCodeUpdate);
}
Expand Down
8 changes: 0 additions & 8 deletions packages/qwik-city/middleware/request-handler/page-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,6 @@ function getPrefetchBundleNames(result: RenderResult, routeBundleNames: string[]
addBundle(manifest.mapping[renderedSymbolName]);
}
}

// and the router regex map
for (const [bundleName, bundle] of Object.entries(manifest.bundles)) {
if (bundle.origins && bundle.origins.includes('@qwik-city-plan')) {
addBundle(bundleName);
break;
}
}
}

if (routeBundleNames) {
Expand Down
4 changes: 4 additions & 0 deletions packages/qwik-city/runtime/src/library/client-navigate.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { QPrefetchData } from './service-worker/types';
import type { RouteNavigate, SimpleURL } from './types';
import { isSameOriginDifferentPathname, isSamePath, toPath, toUrl } from './utils';

Expand Down Expand Up @@ -88,6 +89,9 @@ const scrollToHashId = (doc: Document, hash: string) => {
return elm;
};

export const dispatchPrefetchEvent = (prefetchData: QPrefetchData) =>
dispatchEvent(new CustomEvent('qprefetch', { detail: prefetchData }));

export const CLIENT_HISTORY_INITIALIZED = /* @__PURE__ */ Symbol();

export interface ClientHistoryWindow extends Window {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { AppBundles } from './types';
import type { AppBundle, LinkBundle } from './types';
import { setupServiceWorkerScope } from './setup';

/**
* @alpha
*/
export const setupServiceWorker = () => {
if (typeof self !== 'undefined' && typeof appBundles !== 'undefined') {
setupServiceWorkerScope(self as any, appBundles);
setupServiceWorkerScope(self as any, appBundles, libraryBundleIds, linkBundles);
}
};

declare const appBundles: AppBundles;
declare const appBundles: AppBundle[];
declare const libraryBundleIds: number[];
declare const linkBundles: LinkBundle[];
102 changes: 80 additions & 22 deletions packages/qwik-city/runtime/src/library/service-worker/prefetch.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,97 @@
import type { Fetch, AppBundles } from './types';
import { cachedFetch } from './cached-fetch';
import type { AppBundle, Fetch, LinkBundle } from './types';
import { awaitingRequests, existingPrefetches } from './constants';
import { cachedFetch } from './cached-fetch';
import { getAppBundleByName, getAppBundlesNamesFromIds } from './utils';

export const prefetchBundleNames = (
appBundles: AppBundles,
appBundles: AppBundle[],
qBuildCache: Cache,
fetch: Fetch,
baseUrl: URL,
activeDomQKeys: string[] | undefined,
prefetchAppBundleNames: string[]
prefetchAppBundleNames: (string | null)[] | undefined | null
) => {
const prefetchAppBundle = (prefetchAppBundleName: string) => {
const appBundle = appBundles[prefetchAppBundleName];
const prefetchAppBundle = (prefetchAppBundleName: string | null) => {
try {
const appBundle = getAppBundleByName(appBundles, prefetchAppBundleName);

if (appBundle && !existingPrefetches.has(prefetchAppBundleName)) {
try {
existingPrefetches.add(prefetchAppBundleName);
if (appBundle && !existingPrefetches.has(prefetchAppBundleName!)) {
existingPrefetches.add(prefetchAppBundleName!);

const [importedBundleNames, symbolHashesInBundle] = appBundle;
const importedBundleNames = getAppBundlesNamesFromIds(appBundles, appBundle[1]);
const url = new URL(prefetchAppBundleName!, baseUrl);
const request = new Request(url);

const symbolActiveInDom =
Array.isArray(activeDomQKeys) &&
activeDomQKeys.some((qKey) => symbolHashesInBundle.includes(qKey));

if (!symbolActiveInDom) {
const url = new URL(prefetchAppBundleName, baseUrl).href;
cachedFetch(qBuildCache, fetch, awaitingRequests, new Request(url));
}
cachedFetch(qBuildCache, fetch, awaitingRequests, request);

importedBundleNames.forEach(prefetchAppBundle);
} catch (e) {
console.error(e);
}
} catch (e) {
console.error(e);
}
};

prefetchAppBundleNames.forEach(prefetchAppBundle);
if (Array.isArray(prefetchAppBundleNames)) {
prefetchAppBundleNames.forEach(prefetchAppBundle);
}
};

export const prefetchLinkBundles = (
appBundles: AppBundle[],
libraryBundleIds: number[],
linkBundles: LinkBundle[],
qBuildCache: Cache,
fetch: Fetch,
baseUrl: URL,
linkPathnames: string[]
) => {
try {
prefetchBundleNames(
appBundles,
qBuildCache,
fetch,
baseUrl,
getAppBundlesNamesFromIds(appBundles, libraryBundleIds)
);
} catch (e) {
console.error(e);
}

for (const linkPathname of linkPathnames) {
try {
for (const linkBundle of linkBundles) {
const [route, linkBundleIds] = linkBundle;
console;
if (route.test(linkPathname)) {
prefetchBundleNames(
appBundles,
qBuildCache,
fetch,
baseUrl,
getAppBundlesNamesFromIds(appBundles, linkBundleIds)
);
break;
}
}
} catch (e) {
console.error(e);
}
}
};

export const prefetchWaterfall = (
appBundles: AppBundle[],
qBuildCache: Cache,
fetch: Fetch,
requestedBuildUrl: URL
) => {
try {
const segments = requestedBuildUrl.href.split('/');
const requestedBundleName = segments[segments.length - 1];
segments[segments.length - 1] = '';
const baseUrl = new URL(segments.join('/'));

prefetchBundleNames(appBundles, qBuildCache, fetch, baseUrl, [requestedBundleName]);
} catch (e) {
console.error(e);
}
};
36 changes: 22 additions & 14 deletions packages/qwik-city/runtime/src/library/service-worker/setup.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import type { AppBundles, ServiceWorkerMessageEvent } from './types';
import type { AppBundle, LinkBundle, ServiceWorkerMessageEvent } from './types';
import { awaitingRequests, qBuildCacheName } from './constants';
import { cachedFetch } from './cached-fetch';
import { getCacheToDelete, isAppBundleRequest } from './utils';
import { prefetchBundleNames } from './prefetch';
import { prefetchBundleNames, prefetchLinkBundles, prefetchWaterfall } from './prefetch';

export const setupServiceWorkerScope = (
swScope: ServiceWorkerGlobalScope,
appBundles: AppBundles
appBundles: AppBundle[],
libraryBundleIds: number[],
linkBundles: LinkBundle[]
) => {
swScope.addEventListener('fetch', (ev) => {
const request = ev.request;
Expand All @@ -17,30 +19,36 @@ export const setupServiceWorkerScope = (
if (isAppBundleRequest(appBundles, url.pathname)) {
const nativeFetch = swScope.fetch.bind(swScope);
ev.respondWith(
swScope.caches
.open(qBuildCacheName)
.then((qrlCache) => cachedFetch(qrlCache, nativeFetch, awaitingRequests, request))
swScope.caches.open(qBuildCacheName).then((qBuildCache) => {
prefetchWaterfall(appBundles, qBuildCache, nativeFetch, url);
return cachedFetch(qBuildCache, nativeFetch, awaitingRequests, request);
})
);
}
}
});

swScope.addEventListener('message', async ({ data }: ServiceWorkerMessageEvent) => {
if (data.type === 'qprefetch' && typeof data.base === 'string') {
if (Array.isArray(data.bundles)) {
const nativeFetch = swScope.fetch.bind(swScope);
const qBuildCache = await swScope.caches.open(qBuildCacheName);
const baseUrl = new URL(data.base, swScope.origin);
const nativeFetch = swScope.fetch.bind(swScope);
const qBuildCache = await swScope.caches.open(qBuildCacheName);
const baseUrl = new URL(data.base, swScope.origin);

prefetchBundleNames(
if (Array.isArray(data.links)) {
prefetchLinkBundles(
appBundles,
libraryBundleIds,
linkBundles,
qBuildCache,
nativeFetch,
fetch,
baseUrl,
data.qKeys,
data.bundles
data.links
);
}

if (Array.isArray(data.bundles)) {
prefetchBundleNames(appBundles, qBuildCache, nativeFetch, baseUrl, data.bundles);
}
}
});

Expand Down
Loading

0 comments on commit 49250a1

Please sign in to comment.