Skip to content

Commit

Permalink
preload option, some docs
Browse files Browse the repository at this point in the history
  • Loading branch information
dummdidumm committed Nov 16, 2022
1 parent 81186b6 commit 20c6df1
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 31 deletions.
5 changes: 5 additions & 0 deletions .changeset/odd-bears-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

[feat] preload fonts and add preload customization
4 changes: 3 additions & 1 deletion documentation/docs/30-advanced/20-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,16 @@ You can add call multiple `handle` functions with [the `sequence` helper functio

- `transformPageChunk(opts: { html: string, done: boolean }): MaybePromise<string | undefined>` — applies custom transforms to HTML. If `done` is true, it's the final chunk. Chunks are not guaranteed to be well-formed HTML (they could include an element's opening tag but not its closing tag, for example) but they will always be split at sensible boundaries such as `%sveltekit.head%` or layout/page components.
- `filterSerializedResponseHeaders(name: string, value: string): boolean` — determines which headers should be included in serialized responses when a `load` function loads a resource with `fetch`. By default, none will be included.
- `preload(input: { type: 'js' | 'css' | 'font' | 'asset', path: string }): boolean` — determines what should be added to the `<head>` tag to preload it. Preloading can improve performance because things are downloaded sooner, but they can also hurt core web vitals because too many things may be downloaded unnecessarily. By default, `js`, `css` and `font` files will be preloaded. `asset` files are not preloaded at all currently, but we may add this later after evaluating feedback.

```js
/// file: src/hooks.server.js
/** @type {import('@sveltejs/kit').Handle} */
export async function handle({ event, resolve }) {
const response = await resolve(event, {
transformPageChunk: ({ html }) => html.replace('old', 'new'),
filterSerializedResponseHeaders: (name) => name.startsWith('x-')
filterSerializedResponseHeaders: (name) => name.startsWith('x-'),
preload: ({ type, path }) => type === 'js' || path.includes('/important/')
});

return response;
Expand Down
12 changes: 9 additions & 3 deletions packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@ import { Redirect } from '../control.js';

/* global __SVELTEKIT_ADAPTER_NAME__ */

/** @param {{ html: string }} opts */
/** @type {import('types').RequiredResolveOptions['transformPageChunk']} */
const default_transform = ({ html }) => html;

/** @type {import('types').RequiredResolveOptions['filterSerializedResponseHeaders']} */
const default_filter = () => false;

/** @type {import('types').RequiredResolveOptions['preload']} */
const default_preload = ({ type }) => type !== 'asset';

/** @type {import('types').Respond} */
export async function respond(request, options, state) {
let url = new URL(request.url);
Expand Down Expand Up @@ -185,7 +189,8 @@ export async function respond(request, options, state) {
/** @type {import('types').RequiredResolveOptions} */
let resolve_opts = {
transformPageChunk: default_transform,
filterSerializedResponseHeaders: default_filter
filterSerializedResponseHeaders: default_filter,
preload: default_preload
};

/**
Expand All @@ -211,7 +216,8 @@ export async function respond(request, options, state) {

resolve_opts = {
transformPageChunk: opts.transformPageChunk || default_transform,
filterSerializedResponseHeaders: opts.filterSerializedResponseHeaders || default_filter
filterSerializedResponseHeaders: opts.filterSerializedResponseHeaders || default_filter,
preload: opts.preload || default_preload
};
}

Expand Down
64 changes: 37 additions & 27 deletions packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,36 +224,43 @@ export async function render_response({

for (const dep of stylesheets) {
const path = prefixed(dep);
const attributes = [];

if (csp.style_needs_nonce) {
attributes.push(`nonce="${csp.nonce}"`);
}
if (resolve_opts.preload({ type: 'css', path })) {
const attributes = [];

if (inline_styles.has(dep)) {
// don't load stylesheets that are already inlined
// include them in disabled state so that Vite can detect them and doesn't try to add them
attributes.push('disabled', 'media="(max-width: 0)"');
} else {
const preload_atts = ['rel="preload"', 'as="style"'].concat(attributes);
link_header_preloads.add(`<${encodeURI(path)}>; ${preload_atts.join(';')}; nopush`);
}
if (csp.style_needs_nonce) {
attributes.push(`nonce="${csp.nonce}"`);
}

if (inline_styles.has(dep)) {
// don't load stylesheets that are already inlined
// include them in disabled state so that Vite can detect them and doesn't try to add them
attributes.push('disabled', 'media="(max-width: 0)"');
} else {
const preload_atts = ['rel="preload"', 'as="style"'].concat(attributes);
link_header_preloads.add(`<${encodeURI(path)}>; ${preload_atts.join(';')}; nopush`);
}

attributes.unshift('rel="stylesheet"');
head += `\n\t\t<link href="${path}" ${attributes.join(' ')}>`;
attributes.unshift('rel="stylesheet"');
head += `\n\t\t<link href="${path}" ${attributes.join(' ')}>`;
}
}

for (const dep of fonts) {
const ext = dep.slice(dep.lastIndexOf('.') + 1);
const attributes = [
'rel="preload"',
'as="font"',
`type="font/${ext}"`,
`href="${prefixed(dep)}"`,
'crossorigin'
];

head += `\n\t\t<link ${attributes.join(' ')}>`;
const path = prefixed(dep);

if (resolve_opts.preload({ type: 'font', path })) {
const ext = dep.slice(dep.lastIndexOf('.') + 1);
const attributes = [
'rel="preload"',
'as="font"',
`type="font/${ext}"`,
`href="${path}"`,
'crossorigin'
];

head += `\n\t\t<link ${attributes.join(' ')}>`;
}
}

if (page_config.csr) {
Expand All @@ -280,9 +287,12 @@ export async function render_response({

for (const dep of modulepreloads) {
const path = prefixed(dep);
link_header_preloads.add(`<${encodeURI(path)}>; rel="modulepreload"; nopush`);
if (state.prerendering) {
head += `\n\t\t<link rel="modulepreload" href="${path}">`;

if (resolve_opts.preload({ type: 'js', path })) {
link_header_preloads.add(`<${encodeURI(path)}>; rel="modulepreload"; nopush`);
if (state.prerendering) {
head += `\n\t\t<link rel="modulepreload" href="${path}">`;
}
}
}

Expand Down
24 changes: 24 additions & 0 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,12 @@ export interface KitConfig {
};
}

/**
* This function runs every time the SvelteKit server receives a [request](https://kit.svelte.dev/docs/web-standards#fetch-apis-request) and
* determines the [response](https://kit.svelte.dev/docs/web-standards#fetch-apis-response).
* It receives an `event` object representing the request and a function called `resolve`, which renders the route and generates a `Response`.
* This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing routes programmatically, for example).
*/
export interface Handle {
(input: {
event: RequestEvent;
Expand Down Expand Up @@ -590,8 +596,26 @@ export interface RequestHandler<
}

export interface ResolveOptions {
/**
* Applies custom transforms to HTML. If `done` is true, it's the final chunk. Chunks are not guaranteed to be well-formed HTML
* (they could include an element's opening tag but not its closing tag, for example)
* but they will always be split at sensible boundaries such as `%sveltekit.head%` or layout/page components.
* @param input the html chunk and the info if this is the last chunk
*/
transformPageChunk?(input: { html: string; done: boolean }): MaybePromise<string | undefined>;
/**
* Determines which headers should be included in serialized responses when a `load` function loads a resource with `fetch`.
* By default, none will be included.
* @param name header name
* @param value header value
*/
filterSerializedResponseHeaders?(name: string, value: string): boolean;
/**
* Determines what should be added to the `<head>` tag to preload it.
* By default, `js`, `css` and `font` files will be preloaded.
* @param input the type of the file and its path
*/
preload?(input: { type: 'font' | 'css' | 'js' | 'asset'; path: string }): boolean;
}

export class Server {
Expand Down

0 comments on commit 20c6df1

Please sign in to comment.