做SvelteKit服务端渲染页面时的分析、优化和懒加载vendor.js

Analyzing, optimizing and lazy loading vendor.js when doing SvelteKit server-side rendered pages

我是building a website with SvelteKit. SvelteKit makes things a blast, especially server-side rendering is easy. Currently, I am serving the website using SvelteKit's node.js adapter,因为数据库生成的网页数量很大。

但是,根据 PageSpeed Insights

,尽管有服务器端呈现,但我仍面临移动连接性能低下的问题

First Contentful Paint 很慢(4.0 秒),我自己也很难接受。

我分析这个问题主要是为了阻止 vendor.js 的加载。

Node.js 适配器加载 vendor.js 如下:

<link rel="modulepreload" href="/_app/chunks/vendor-87f5cdbf.js">

其他一些 modulepreload 包也包括在内,但它们不会导致很长的加载时间

vendor.js 是 350kb 并且是网站加载最阻塞的原因。

我的问题是

My frontend is open source 了解更多信息。我正在使用 svelte-bootstrap 库。

一个使某些 javascript 异步加载的 hacky 解决方案是在您的 hooks.ts 文件中创建一个 handle 挂钩。

用它你可以编辑发送给客户端的html,并用<script defer async>替换<link rel="module preload">

看起来像这样:

// horrible hack
export async function handle({ request, resolve }: Parameters<Handle>[0]): Promise<ServerResponse> {
  const response = await resolve(request);

  if (typeof response.body === "string") { // not sure if necessary
    response.body = response.body.replace(
      /<link rel="modulepreload" href="(\/_app\/chunks\/vendor-[^.]+\.js)">/, 
      (_, url) => `<script defer async src="${url}">`
    );
  }

  return response;
}

请注意,如果 SvelteKit 发出的 html 格式发生变化,它很容易崩溃。

下面是一个完整的transformer <link rel="modulepreload"> -> <script type="module" async">钩子基于cheetah library. Note that this transformer adds SSR loading penalty and the proper solution is to fix Vite.

改造

The frontpage now achievees the perfect 100 score on PageSpeed test 用于桌面连接,90 用于移动连接。

  • 标签使用 body 钩子移动到 `` 的末尾,灵感来自 ferom coyotte508 的答案
  • 这将确保 CSS 文件始终首先加载并且 First Contentful Paint 速度很快,之后网络浏览器开始加载 JavaScript 以使页面具有交互性

  • 我设法改进了 Mobile 3G FCP from 4 seconds to 2.5 seconds,这是对用户体验的重大改进。对于慢速 3G,这个数字从 9 多秒减少到 3.5 秒,这是更显着的改进。

我必须做的其他优化:

  • Manually optimize Boostrap CSS bundle 通过删除不需要的组件并减少自动生成的 CSS class 变体的数量(alert-xxxxmt- 等。 ).原始包未压缩超过 500 kb,当前包未压缩为 231 kb。

  • Self-host Google fonts - 减少移动连接的一些 TLS 握手时间

  • 手动删除 Svelte 组件中导入的一些小 CSS 文件,并使用 :global 选择器将这些样式移动到 __layout.svelte 以减少为CSS 个文件

  • 确保所有 <img> 标签都设置了宽度和高度属性以减少累积布局偏移

  • Enable extrernalFetch hook on SSR 通过优化后端来减少 SSR 页面加载时间 API 调用往返时间

  • 。我不知道 SvelteKit + Vite + Rollup 如何交互,但这似乎在未生成大量 vendor.js 块时在移动页面加载上额外提供 0.2 秒。

My hooks.ts:

import type { Handle } from '@sveltejs/kit';
import * as cheerio from 'cheerio';
import {siteMode} from "$lib/config";


/**
 * Modifies the response in-place to fix the link rel="modulepreload" issue.
 *
 * Note that this fix causes significant server-side processing time increase -
 * do not use long term, only proper fix is to fix Vite.
 *
 * For the source of the issue of loading times please see https://github.com/vitejs/vite/issues/5120
 *
 */
function fixLinkModulePreloadIssue(response: ServerResponse) {

    if(!response.body) {
        throw new Error("text/html response was missing body");
    }
    const $ = cheerio.load(response.body);

    const body = $("body");

    // Replace <link rel="modulepreload"> with <script type="module">
    // https://api.jquery.com/replacewith/
    //$('link[rel="modulepreload"]').replaceWith(function() {
    //  const src = $(this).attr('href');
    //// The defer attribute has no effect on module scripts — they defer by default.
    //  return `<script type="module" src="${src}"></script>`;
    //});

    $('link[rel="modulepreload"]').each(function(idx, elem) {
        const $this = $(this);
        const src = $this.attr('href');
        $(`<script type="module" async src="${src}"></script>`).appendTo(body);
        $this.remove();
    });

    // TODO: Does not have effect if we put scripts at the end of the body
    // Move starter section to the <body> end
    // Assume we have only one <script type="module"> in our generated <head>
    // const starter = $("head script[type='module']");
    //starter.appendTo(body);
    //starter.remove();

    response.body = $.html();
}


export const handle: Handle = async ({ request, resolve }) => {

    // TODO https://github.com/sveltejs/kit/issues/1046
    if (request.query.has('_method')) {
        request.method = request.query.get('_method').toUpperCase();
    }

    const response = await resolve(request);

    // Fix modulepreload issue
    // 

    if (response.headers["content-type"] === "text/html") {
        // Only try to transform HTML pages,
        // do not touch binary etc. loads
        fixLinkModulePreloadIssue(response);
    }

    return response;
};


/**
 * Shortcut fetch() API requests on the server-side rendering.
 *
 * See https://github.com/tradingstrategy-ai/proxy-server/blob/master/Caddyfile
 *
 * @type {import('@sveltejs/kit').ExternalFetch}
 */
export async function externalFetch(request) {

    if(siteMode == "production") {

        // Write API URL to the internal network
        // TODO: Make these URLs part of config
        const publicHost = "https://tradingstrategy.ai/api";
        const internalHost = "http://127.0.0.1:3456"

        if (request.url.startsWith(publicHost)) {
            // clone the original request, but change the URL
            request = new Request(
                request.url.replace(publicHost, internalHost),
                request
            );}

    }
    return fetch(request);
}

与其对 modulepreload 脚本进行这种复杂的重写,不如禁用供应商块:

export default {
    kit: {
        vite: {
            build: {
                rollupOptions: {
                    output: {
                        manualChunks: undefined
                    }
                }
            }
        }
    }
}

Vite 问题跟踪器中的更多信息:https://github.com/vitejs/vite/issues/3731