关于我将 FCP 缩短一半这件事

最近需要搭建一个后台项目,在完成开发后,我将其重构,将 FCP 从平均 2.8 s 缩短到平均 1.2 s。

背景

这个后台项目有点特殊之处在于他的每个页面都将在另个一项目里以 iframe 的形式嵌入。很快就搞定了,技术栈就是 vite + react 等。不过产品在开发之后提了一个需求,在其中一个页面需要支持展示 pag 格式的文件。pag 就是腾讯出个一个类似 lottie 的动效框架,这里不展开。

在引入了 pag 后,我发现页面的加载速度变慢了些,当然这对于后台项目来说倒也不是什么大问题(又不是不能用)。不过刚好有时间研究,调出 performance 面板一看,好家伙,已经 fcp 到了 2.8 s 左右。

pag 推荐的加载方式是在 html 里插入 script,由此我在想我仅仅是其中一个页面要使用到 pag,然而拖慢了所有页面,这合理吗?由于 vite + spa 的方式入口仅一个 html,这个问题无法避免,那么解决的方案可以考虑改造成多页架构。

astro

这时候我想到 astro 的架构,正是 mpa,并且还详细讨论了 spa 与 mpa 之间的对比,那么我就可以在有需要的页面引入 pag 脚本,这样大概率是能优化上述问题的。

至此我打算用 astro 写一个 demo 来简单测试下页面的性能,不过在此之前,我简单过了一下 astro 的文档,并罗列了一 astro 契合当前项目的几个点

  1. 当前项目使用 vite 开发,astro 刚好也是 vite,那么就打包来说迁移成本不高

  2. 当前项目的页面多为独立页面,每个页面都将在另一个页面里作为 iframe 使用,这点也契合 mpa

  3. astro 支持 react 等不同的 ui 框架,当前项目也是基于 react,就业务代码而言迁移成本也不高

  4. astro 支持 ssr 与 ssg,当前项目为纯前端,只需要构建出静态资源并部署到服务端,那么我们不开启 ssr 即可,构建部署方面迁移成本不高

  5. astro 语法本身接近 vue,那么上手成本也不高

那么有了上述几点,我觉得这个方案是大概率可行的

Demo

首先写一个简单的 demo 验证下,迁移的细节不多说

  1. astro 初始化一个项目

  2. 将原先 react-router 管理的页面迁移一个到 src/pages 下

  3. 创建 index.astro ,其中内容为

    ---
    import App from './App';
    ---
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width" />
        <title>Astro Demo</title>
      </head>
      <body>
        <App client:visible />
      </body>
    </html>

    App 即为原页面的组件

通过上述一番腾挪后,项目成功运行起来,FCP 时间降到 1.2s,效果喜人。

解释一下 ``client:visible 表示当组件可见时,开始加载该组件的 js,也就是 astro 的岛屿架构,详细不展开,可参考文档

重构

既然验证是可行的,那么就将这个方案抛出来看看是否能通过(由于是公共的项目),在得到了肯定的答复之后开始动手重构

顺带一提,项目使用 @nxtensions/astro 初始化,这是因为其他业务也将在这个项目里开发,因此用 nx 作为 monorepo 框架。

期间还加入了 plop / tailwindcss / jss / react-query 等,在此就不展开

最后在多次测试下,页面平均 FCP 降低到 1.2s 左右,比原先的 2.8s 减少了 50% 以上。

踩过的坑

  1. astro 不支持 vite 自定义 mode https://github.com/withastro/astro/issues/3311

    解决方案:创建多个 astro config,不同命令加载不同 config

  2. 无法在 astro 配置文件里获取环境参数。解决方案同 1,根据不同命令判断环境

  3. 无法指定 base https://github.com/withastro/astro/issues/4007 astro 会强行将 base 替换成 / 开头

    解决方案:项目中使用到 base 的仅在构建时,作用类似于 public path,那么我写了一个插件,在 astro 构建完成后,将 / 开头的静态资源替换回去即可。

总结

之所以能缩短 FCP 时间,我认为主要是以下几点:

  1. astro 将单页拆分为多页,减少了不必要的其他页面资源的加载时间(如 pag)

  2. astro 的岛屿架构,使得组件在展示之后才开始加载 js

  3. astro 的 ssg,将 react 的模板在构建时就写入 html

然而 astro 是否适合,还需要根据具体项目来判断,此次尝试验证了该方案时可行的,也为将来优化移动端页面提供了一个思路与方向。