Skip to content

[feat] inlineCss option #2620

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 31 commits into from
Jan 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6f53f01
feat: inline CSS
untp Oct 17, 2021
3d0991e
Update .changeset/tiny-files-smile.md
untp Oct 26, 2021
3323960
Update inlineCSS to inlineCss
untp Oct 26, 2021
7122f6a
more docs
untp Oct 26, 2021
319cffe
merge master -> inline-css
Rich-Harris Jan 2, 2022
61b626f
fix merge conflict
Rich-Harris Jan 2, 2022
7c9c909
reinstate functionality
Rich-Harris Jan 3, 2022
fb25dee
lint
Rich-Harris Jan 3, 2022
7c65438
fix
Rich-Harris Jan 3, 2022
c9e72b9
use styles_lookup
Rich-Harris Jan 3, 2022
f5ff2ef
merge master -> inline-css
Rich-Harris Jan 3, 2022
fec7364
tweak docs
Rich-Harris Jan 3, 2022
f73c97f
add disabled links when inlining CSS, tidy up render code
Rich-Harris Jan 4, 2022
0950e16
fix test
Rich-Harris Jan 4, 2022
a0afe6c
Merge branch 'master' into inline-css
Rich-Harris Jan 4, 2022
a6fc60a
use threshold instead all or nothing
untp Jan 4, 2022
31cb619
Merge branch 'sveltejs:master' into inline-css
untp Jan 4, 2022
8d91f8f
inline svelte-announcer style
untp Jan 5, 2022
0a2aed6
Merge branch 'sveltejs:master' into inline-css
untp Jan 5, 2022
7da4a7d
test big css files not to be inlined
untp Jan 5, 2022
bc79b0a
convert indentation to tabs
untp Jan 5, 2022
ce7218d
merge master -> inline-css
Rich-Harris Jan 11, 2022
1f72ea0
tweak test
Rich-Harris Jan 11, 2022
1b041a0
add comment to test
Rich-Harris Jan 11, 2022
8d404cc
merge master -> inline-css
Rich-Harris Jan 11, 2022
296508e
remove merge conflict evidence
Rich-Harris Jan 11, 2022
6d1ca34
simplify announcer styles
Rich-Harris Jan 11, 2022
148968f
dedupe stylesheets between nodes
Rich-Harris Jan 11, 2022
d91c11e
rename inlineCss to inlineStyleThreshold
Rich-Harris Jan 11, 2022
75d43bf
lint
Rich-Harris Jan 11, 2022
96e824b
Update .changeset/tiny-files-smile.md
Rich-Harris Jan 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tiny-files-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

add inlineStyleThreshold option, below which stylesheets are inlined into the page
7 changes: 7 additions & 0 deletions documentation/docs/14-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const config = {
},
host: null,
hydrate: true,
inlineStyleThreshold: 0,
methodOverride: {
parameter: '_method',
allowed: []
Expand Down Expand Up @@ -138,6 +139,12 @@ A value that overrides the one derived from [`config.kit.headers.host`](#configu

Whether to [hydrate](#ssr-and-javascript-hydrate) the server-rendered HTML with a client-side app. (It's rare that you would set this to `false` on an app-wide basis.)

### inlineStyleThreshold

Inline CSS inside a `<style>` block at the head of the HTML. This option is a number that specifies the maximum length of a CSS file to be inlined. All CSS files needed for the page and smaller than this value are merged and inlined in a `<style>` block.

> # This results in fewer initial requests and can improve your [First Contentful Paint](https://web.dev/first-contentful-paint) score. However, it generates larger HTML output and reduces the effectiveness of browser caches. Use it advisedly.

### methodOverride

See [HTTP Method Overrides](#routing-endpoints-http-method-overrides). An object containing zero or more of the following:
Expand Down
56 changes: 39 additions & 17 deletions packages/kit/src/core/build/build_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -263,34 +263,56 @@ export async function build_server(
/** @type {import('vite').Manifest} */
const vite_manifest = JSON.parse(fs.readFileSync(`${output_dir}/server/manifest.json`, 'utf-8'));

const styles_lookup = new Map();
if (config.kit.amp) {
client.assets.forEach((asset) => {
if (asset.fileName.endsWith('.css')) {
styles_lookup.set(asset.fileName, asset.source);
mkdirp(`${output_dir}/server/nodes`);
mkdirp(`${output_dir}/server/stylesheets`);

const stylesheet_lookup = new Map();

client.assets.forEach((asset) => {
if (asset.fileName.endsWith('.css')) {
if (config.kit.amp || asset.source.length < config.kit.inlineStyleThreshold) {
const index = stylesheet_lookup.size;
const file = `${output_dir}/server/stylesheets/${index}.js`;

fs.writeFileSync(file, `// ${asset.fileName}\nexport default ${s(asset.source)};`);
stylesheet_lookup.set(asset.fileName, index);
}
});
}
}
});

mkdirp(`${output_dir}/server/nodes`);
manifest_data.components.forEach((component, i) => {
const file = `${output_dir}/server/nodes/${i}.js`;

const js = new Set();
const css = new Set();
find_deps(component, client.vite_manifest, js, css);

const styles = config.kit.amp && Array.from(css).map((file) => styles_lookup.get(file));
const imports = [`import * as module from '../${vite_manifest[component].file}';`];

const exports = [
'export { module };',
`export const entry = '${client.vite_manifest[component].file}';`,
`export const js = ${s(Array.from(js))};`,
`export const css = ${s(Array.from(css))};`
];

const node = `import * as module from '../${vite_manifest[component].file}';
export { module };
export const entry = '${client.vite_manifest[component].file}';
export const js = ${JSON.stringify(Array.from(js))};
export const css = ${JSON.stringify(Array.from(css))};
${styles ? `export const styles = ${s(styles)}` : ''}
`.replace(/^\t\t\t/gm, '');
/** @type {string[]} */
const styles = [];

css.forEach((file) => {
if (stylesheet_lookup.has(file)) {
const index = stylesheet_lookup.get(file);
const name = `stylesheet_${index}`;
imports.push(`import ${name} from '../stylesheets/${index}.js';`);
styles.push(`\t${s(file)}: ${name}`);
}
});

if (styles.length > 0) {
exports.push(`export const styles = {\n${styles.join(',\n')}\n};`);
}

fs.writeFileSync(file, node);
fs.writeFileSync(file, `${imports.join('\n')}\n\n${exports.join('\n')}\n`);
});

return {
Expand Down
2 changes: 2 additions & 0 deletions packages/kit/src/core/config/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ test('fills in defaults', () => {
},
host: null,
hydrate: true,
inlineStyleThreshold: 0,
methodOverride: {
parameter: '_method',
allowed: []
Expand Down Expand Up @@ -146,6 +147,7 @@ test('fills in partial blanks', () => {
},
host: null,
hydrate: true,
inlineStyleThreshold: 0,
methodOverride: {
parameter: '_method',
allowed: []
Expand Down
2 changes: 2 additions & 0 deletions packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ const options = object(

hydrate: boolean(true),

inlineStyleThreshold: number(0),

methodOverride: object({
parameter: string('_method'),
allowed: validate([], (input, keypath) => {
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/core/config/test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ test('load default config (esm)', async () => {
},
host: null,
hydrate: true,
inlineStyleThreshold: 0,
methodOverride: {
parameter: '_method',
allowed: []
Expand Down
16 changes: 1 addition & 15 deletions packages/kit/src/core/create_app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,25 +178,11 @@ function generate_app(manifest_data) {
${pyramid.replace(/\n/g, '\n\t\t')}

{#if mounted}
<div id="svelte-announcer" aria-live="assertive" aria-atomic="true">
<div id="svelte-announcer" aria-live="assertive" aria-atomic="true" style="position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px">
{#if navigated}
{title}
{/if}
</div>
{/if}

<style>
#svelte-announcer {
position: absolute;
left: 0;
top: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
overflow: hidden;
white-space: nowrap;
width: 1px;
height: 1px;
}
</style>
`);
}
7 changes: 4 additions & 3 deletions packages/kit/src/core/dev/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ export async function create_plugin(config, output, cwd) {
const deps = new Set();
find_deps(node, deps);

const styles = new Set();
/** @type {Record<string, string>} */
const styles = {};

for (const dep of deps) {
const parsed = new URL(dep.url, 'http://localhost/');
Expand All @@ -78,7 +79,7 @@ export async function create_plugin(config, output, cwd) {
) {
try {
const mod = await vite.ssrLoadModule(dep.url);
styles.add(mod.default);
styles[dep.url] = mod.default;
} catch {
// this can happen with dynamically imported modules, I think
// because the Vite module graph doesn't distinguish between
Expand All @@ -92,7 +93,7 @@ export async function create_plugin(config, output, cwd) {
entry: url.endsWith('.svelte') ? url : url + '?import',
css: [],
js: [],
styles: Array.from(styles)
styles
};
};
}),
Expand Down
147 changes: 67 additions & 80 deletions packages/kit/src/runtime/server/page/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ export async function render_response({
}) {
const css = new Set(options.manifest._.entry.css);
const js = new Set(options.manifest._.entry.js);
const styles = new Set();
/** @type {Map<string, string>} */
const styles = new Map();

/** @type {Array<{ url: string, body: string, json: string }>} */
const serialized_data = [];
Expand All @@ -53,7 +54,7 @@ export async function render_response({
branch.forEach(({ node, loaded, fetched, uses_credentials }) => {
if (node.css) node.css.forEach((url) => css.add(url));
if (node.js) node.js.forEach((url) => js.add(url));
if (node.styles) node.styles.forEach((content) => styles.add(content));
if (node.styles) Object.entries(node.styles).forEach(([k, v]) => styles.set(k, v));

// TODO probably better if `fetched` wasn't populated unless `hydrate`
if (fetched && page_config.hydrate) serialized_data.push(...fetched);
Expand Down Expand Up @@ -114,94 +115,80 @@ export async function render_response({
rendered = { head: '', html: '', css: { code: '', map: null } };
}

const include_js = page_config.router || page_config.hydrate;
if (!include_js) js.clear();

// TODO strip the AMP stuff out of the build if not relevant
const links = options.amp
? styles.size > 0 || rendered.css.code.length > 0
? `<style amp-custom>${Array.from(styles).concat(rendered.css.code).join('\n')}</style>`
: ''
: [
// From https://web.dev/priority-hints/:
// Generally, preloads will load in the order the parser gets to them for anything above "Medium" priority
// Thus, we should list CSS first
...Array.from(css).map((dep) => `<link rel="stylesheet" href="${options.prefix}${dep}">`),
...Array.from(js).map((dep) => `<link rel="modulepreload" href="${options.prefix}${dep}">`)
].join('\n\t\t');

/** @type {string} */
let init = '';
let { head, html: body } = rendered;

const inlined_style = Array.from(styles.values()).join('\n');

if (options.amp) {
init = `
head += `
<style amp-boilerplate>body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}</style>
<noscript><style amp-boilerplate>body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}</style></noscript>
<script async src="https://cdn.ampproject.org/v0.js"></script>`;
init += options.service_worker
? '<script async custom-element="amp-install-serviceworker" src="https://cdn.ampproject.org/v0/amp-install-serviceworker-0.1.js"></script>'
: '';
} else if (include_js) {
// prettier-ignore
init = `<script type="module">
import { start } from ${s(options.prefix + options.manifest._.entry.file)};
start({
target: ${options.target ? `document.querySelector(${s(options.target)})` : 'document.body'},
paths: ${s(options.paths)},
session: ${try_serialize($session, (error) => {
throw new Error(`Failed to serialize session data: ${error.message}`);
})},
route: ${!!page_config.router},
spa: ${!ssr},
trailing_slash: ${s(options.trailing_slash)},
hydrate: ${ssr && page_config.hydrate ? `{
status: ${status},
error: ${serialize_error(error)},
nodes: [
${(branch || [])
.map(({ node }) => `import(${s(options.prefix + node.entry)})`)
.join(',\n\t\t\t\t\t\t')}
],
url: new URL(${s(url.href)}),
params: ${devalue(params)}
}` : 'null'}
});
</script>`;
}

if (options.service_worker && !options.amp) {
init += `<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('${options.service_worker}');
}
</script>`;
}
<script async src="https://cdn.ampproject.org/v0.js"></script>

const head = [
rendered.head,
styles.size && !options.amp
? `<style data-svelte>${Array.from(styles).join('\n')}</style>`
: '',
links,
init
].join('\n\n\t\t');
<style amp-custom>${inlined_style}\n${rendered.css.code}</style>`;

let body = rendered.html;
if (options.amp) {
if (options.service_worker) {
head +=
'<script async custom-element="amp-install-serviceworker" src="https://cdn.ampproject.org/v0/amp-install-serviceworker-0.1.js"></script>';

body += `<amp-install-serviceworker src="${options.service_worker}" layout="nodisplay"></amp-install-serviceworker>`;
}
} else {
body += serialized_data
.map(({ url, body, json }) => {
let attributes = `type="application/json" data-type="svelte-data" data-url=${escape_html_attr(
url
)}`;
if (body) attributes += ` data-body="${hash(body)}"`;

return `<script ${attributes}>${json}</script>`;
})
.join('\n\n\t');
if (inlined_style) {
head += `\n\t<style${options.dev ? ' data-svelte' : ''}>${inlined_style}</style>`;
}
// prettier-ignore
head += Array.from(css)
.map((dep) => `\n\t<link${styles.has(dep) ? ' disabled' : ''} rel="stylesheet" href="${options.prefix + dep}">`)
.join('');

if (page_config.router || page_config.hydrate) {
head += Array.from(js)
.map((dep) => `\n\t<link rel="modulepreload" href="${options.prefix + dep}">`)
.join('');
// prettier-ignore
head += `
<script type="module">
import { start } from ${s(options.prefix + options.manifest._.entry.file)};
start({
target: ${options.target ? `document.querySelector(${s(options.target)})` : 'document.body'},
paths: ${s(options.paths)},
session: ${try_serialize($session, (error) => {
throw new Error(`Failed to serialize session data: ${error.message}`);
})},
route: ${!!page_config.router},
spa: ${!ssr},
trailing_slash: ${s(options.trailing_slash)},
hydrate: ${ssr && page_config.hydrate ? `{
status: ${status},
error: ${serialize_error(error)},
nodes: [
${(branch || [])
.map(({ node }) => `import(${s(options.prefix + node.entry)})`)
.join(',\n\t\t\t\t\t\t')}
],
url: new URL(${s(url.href)}),
params: ${devalue(params)}
}` : 'null'}
});
</script>${options.service_worker ? `
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('${options.service_worker}');
}
</script>` : ''}`;

body += serialized_data
.map(({ url, body, json }) => {
let attributes = `type="application/json" data-type="svelte-data" data-url=${escape_html_attr(
url
)}`;
if (body) attributes += ` data-body="${hash(body)}"`;

return `<script ${attributes}>${json}</script>`;
})
.join('\n\n\t');
}
}

/** @type {import('types/helper').ResponseHeaders} */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div class="svelte-logo"></div>

<style>
.svelte-logo {
width: 107px;
height: 128px;
background: url('data:image/svg+xml;charset=UTF-8,<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.1566,22.8189c-10.4-14.8851-30.94-19.2971-45.7914-9.8348L22.2825,29.6078A29.9234,29.9234,0,0,0,8.7639,49.6506a31.5136,31.5136,0,0,0,3.1076,20.2318A30.0061,30.0061,0,0,0,7.3953,81.0653a31.8886,31.8886,0,0,0,5.4473,24.1157c10.4022,14.8865,30.9423,19.2966,45.7914,9.8348L84.7167,98.3921A29.9177,29.9177,0,0,0,98.2353,78.3493,31.5263,31.5263,0,0,0,95.13,58.117a30,30,0,0,0,4.4743-11.1824,31.88,31.88,0,0,0-5.4473-24.1157" style="fill:%23ff3e00"/><path d="M45.8171,106.5815A20.7182,20.7182,0,0,1,23.58,98.3389a19.1739,19.1739,0,0,1-3.2766-14.5025,18.1886,18.1886,0,0,1,.6233-2.4357l.4912-1.4978,1.3363.9815a33.6443,33.6443,0,0,0,10.203,5.0978l.9694.2941-.0893.9675a5.8474,5.8474,0,0,0,1.052,3.8781,6.2389,6.2389,0,0,0,6.6952,2.485,5.7449,5.7449,0,0,0,1.6021-.7041L69.27,76.281a5.4306,5.4306,0,0,0,2.4506-3.631,5.7948,5.7948,0,0,0-.9875-4.3712,6.2436,6.2436,0,0,0-6.6978-2.4864,5.7427,5.7427,0,0,0-1.6.7036l-9.9532,6.3449a19.0329,19.0329,0,0,1-5.2965,2.3259,20.7181,20.7181,0,0,1-22.2368-8.2427,19.1725,19.1725,0,0,1-3.2766-14.5024,17.9885,17.9885,0,0,1,8.13-12.0513L55.8833,23.7472a19.0038,19.0038,0,0,1,5.3-2.3287A20.7182,20.7182,0,0,1,83.42,29.6611a19.1739,19.1739,0,0,1,3.2766,14.5025,18.4,18.4,0,0,1-.6233,2.4357l-.4912,1.4978-1.3356-.98a33.6175,33.6175,0,0,0-10.2037-5.1l-.9694-.2942.0893-.9675a5.8588,5.8588,0,0,0-1.052-3.878,6.2389,6.2389,0,0,0-6.6952-2.485,5.7449,5.7449,0,0,0-1.6021.7041L37.73,51.719a5.4218,5.4218,0,0,0-2.4487,3.63,5.7862,5.7862,0,0,0,.9856,4.3717,6.2437,6.2437,0,0,0,6.6978,2.4864,5.7652,5.7652,0,0,0,1.602-.7041l9.9519-6.3425a18.978,18.978,0,0,1,5.2959-2.3278,20.7181,20.7181,0,0,1,22.2368,8.2427,19.1725,19.1725,0,0,1,3.2766,14.5024,17.9977,17.9977,0,0,1-8.13,12.0532L51.1167,104.2528a19.0038,19.0038,0,0,1-5.3,2.3287" style="fill:%23fff"/></svg>');
}
</style>
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<script>
import { page } from '$app/stores';
import SvelteLogo from '$lib/SvelteLogo.svelte';
</script>

<SvelteLogo />

<h2>{$page.params.slug}</h2>

<a href="/path-base/base/two">/path-base/base/two</a>
Loading