How to get 100 performance score in Lighthouse with SvelteKit
Lighthouse is a tool for auditing performance, accessibility and SEO of a web page. As I want to give my readers the best browsing experience, I am concerned with the Lighthouse score of my website.
In this post, I will show you a few techniques on optimizing your SvelteKit project for getting 100 performance score on Lighthouse.
Minifying HTML response
Out of the box, only CSS and Javascript is minified by SvelteKit. HTML is not handled.
To minify HTML responses, no matter which adapter you are using, you can transform the HTML response in hooks.server.js
or hooks.server.ts
with an HTML minification library. The following is a snippet of using html-minifier
to minify the HTML response.
import { minify } from 'html-minifier';
import { type Handle } from '@sveltejs/kit';
const minifyOpts = {
collapseBooleanAttributes: true,
collapseWhitespace: true,
conservativeCollapse: true,
decodeEntities: true,
html5: true,
ignoreCustomComments: [/^#/],
minifyCSS: true,
minifyJS: false,
removeAttributeQuotes: true,
removeComments: false,
removeOptionalTags: true,
removeRedundantAttributes: true,
removeScriptTypeAttributes: true,
removeStyleLinkTypeAttributes: true,
sortAttributes: true,
sortClassName: true,
};
export const handle: Handle = async ({ event, resolve }) => {
return resolve(event, {
transformPageChunk: ({ html, done }) => {
return minify(html, minifyOpts);
},
});
};
Enable pre-compression
After minifying your assets, you should also compress them with gzip
or brotli
, reducing the size of files to be transmitted. Some adapters, such as @sveltejs/adapter-node
and @sveltejs/adapter-static
support pre-compression, and you can pre-compress your assets in the build time. Otherwise, you will have to compress your assets on the fly.
import adapter from '@sveltejs/adapter-static';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
precompress: true,
}),
},
};
export default config;
After compressing your assets, you might need to configure your load balancer for serving them. My guide on serving compressed files with nginx
might be useful for you.
Cache immutable assets with Cache-Control
HTTP response header
If you have ever examined the build output of SvelteKit, you may notice that /_app/immutable
contains all your CSS and Javascript of a build (except service worker script, sw.js
), and every file in it has a hash in their file name, which is generated based on the content of the file. These hashes are used for cache-busting, and files with such hash should be cached for as long as possible.
Cache-Control
HTTP response header indicates how long a resource should be cached. You should at least cache files in /_app/immutable
for a year.
Cache-Control: max-age=31536000
Do not add such strong caching rule to a file without hash, unless you know what you are doing. Setting Cache-Control: no-cache
to those files is a good default, which the browser will still cache the file, but always revalidate, making sure the latest version is served.
Depending on what you prefer, you can set this header at your load balancer level or file server level. The configuration depends on what you use. If you are using nginx
, you can add this header inside a location
block.
server {
# omitted for brevity
location /_app/immutable {
add_header Cache-Control "max-age=31536000";
}
}
Optimize images
This is the most important optimization to have, as an unoptimized image can drag down the performance of your website significantly. In principle, optimizing images on a webpage means serving the right format and the right size. You should use format that can be compressed smaller such as webp
or avif
, and you should serve a smaller image, if your user is browsing your website with his phone. In a nutshell, you have to generate images in multiple formats and dimension to achieve this.
Optimize images at build time
In the past I have to set up tools such as sharp
and create an image markup component to load the generated images and the original image as fallback, using <picture>
and <source>
. Fortunately, the new experimental plugin @sveltejs/enhanced-img do all that for us.
<script>
import FooImage from './foo.jpg?enhanced'; // import the source image but generated the enhanced version as well
</script>
<!-- this component is using <picture> and <source> as well, just nicely packaged -->
<enhanced:img src={FooImage} alt="some alt text" />
Optimize images at run time
If your images are stored in a S3 bucket or somewhere remote, and you load them dynamically, you can only optimize them in runtime. You can use an image transformation proxy, such as imgproxy to generate an optimized image on the fly.
As an alternative, you can also run scripts that optimize images continuously with cron jobs.
Enable pre-rendering if possible
By pre-rendering your pages, you create an HTML file with all the variables in your Svelte markup substituted in the build time. Users of your application will then get a hydrated (basically it means completed) HTML file immediately and reduce First Contentful Print. You can set const prerender = true
in your +layout.ts
or +page.ts
to enable pre-rendering. Setting it at +layout.ts
will enable pre-rendering for all pages using that layout.
export const prerender = true;
However, pre-rendering might not be useful to your application, if your users cannot benefit from stale data. Say your application shows real time stocks’ price, pre-rendering and shipping the prices at the build time to users initially is meaningless.
Disable client side rendering (CSR)
If your page can be pre-rendered or server side rendered, and you do not need interactivity on your page, you can simply disable client side rendering and not ship Javascript to your users. This would obviously reduce the payload you send to your users, but it might only be feasible for static pages, such as a blog post.
To disable CSR, you can set const csr = false
in a +layout.ts
or +page.ts
, and it will disable CSR for a set of pages using that layout or an individual page.
export const csr = false;
// enable at least ssr or pre-rendering, otherwise a blank page will be shipped
export const ssr = true;
export const prerender = true;
Optimize font rendering
If no font specified through font-family
has been loaded, browsers would wait for it and would not render text for a period of time. To make browsers fallback to system font, until at least one specified font is available, you should set font-display: swap
in your @font-face
rule in CSS.
@font-face {
font-family: Roboto;
font-display: swap;
}
Also you should only use font with woff2
format, as it can achieve better compression comparing with other formats, and it is supported by almost all browsers in 2025.
If you don’t want to deal with all these manually, I would recommend using font packages from Fontsource. Out of the box they set font-display: swap
and use only woff2
font files.