Critical CSS 在 CSR 项目中的落地

3 阅读1分钟

项目背景

产品类型

移动端政务管家

技术架构

UI 框架:Vue3

路由管理:VueRouter4

状态管理:Pinia3

组件库:Vant4

构建工具:Vite6

CSS:Tailwindcss3

备注:项目采用  unplugin-auto-import/vite、 'unplugin-vue-components/vite'、VantResolver 完成组件与样式的自动引入

实现步骤

由于是传统的 CSR 项目,首页是通过 JavaScript 动态生成的,直接使用 Vite 生态的 Critical CSS 插件无法达到效果,因此选用了 vite-ssg 插件,同时达到预渲染 + Critical CSS 的目的

安装依赖

pnpm add vite-ssg -D

首页改造

import { createPinia } from 'pinia'import { ViteSSG } from 'vite-ssg'import routes from '~pages'import './styles/index.css'import App from './App.vue'export const createApp = ViteSSG(  App,  { routes: routes },  ({ app, router, initialState }) => {    const pinia = createPinia()    app.use(pinia)    if (import.meta.env.SSR) initialState.pinia = pinia.state.value    else pinia.state.value = initialState.pinia || {}    router.beforeEach((to, _from, next) => {      if (to.path === '/') {        next({ path: '/home' })      }      if (to.name === 'index-home') {        const urls = [          `${import.meta.env.VITE_BASE_PATH}img/home/psychological.webp`,        ]        urls.forEach(url => {          preloadImage(url)        })      }      next()    })  },)

Vite 配置

ssgOptions: {  includedRoutes: (paths: string[]) => {    return paths.filter(path => {      return path === 'home'    })  },  mock: true, // 模拟浏览器的全局变量,让代码能够正确地在 node 环境运行  beastiesOptions: { preload: 'media' },},

解决 Vant 样式报错

**报错根本原因:**项目中导入的样式是 Vant ESM 库的导出样式:vant/es/xxx/style/index.mjs

以 button 组件为例,index.mjs 文件的内容如下

import "../../style/base.css";import "../../badge/index.css";import "../../icon/index.css";import "../../loading/index.css";import "../index.css";

在打包后的 JS 文件中包含 import "vant/es/xxx/style/index"在 Node 环境下,直接执行该 JS 代码会报错

解决方案:

  1. import "vant/es/xxx/style/index" 执行不报错
  2. 让 import "vant/es/xxx/style/index" 不出现在打包的文件中

第一种方向没有找到合适的解决方案,第二种方向采用了直接导入 CSS 文件的方式,明细如下:

  • postinstall 阶段执行脚本,在 index.mjs 旁边新建 index.css 文件,css 文件的内容如下

    @import "../../style/base.css"; @import "../../badge/index.css"; @import "../../icon/index.css"; @import "../../loading/index.css"; @import "../index.css";

动态生成的脚本这里就不贴了,可以使用 AI 直接生成,AI 已经可以高效地完成有规律地事情,比如这里,只需要发出指令:在 vant/es/xxx/style/index.mjs 旁边新建一个 vant/es/xxx/style/index.css 文件,css 文件与 mjs 文件地内容一致,xxx 代表具体的组件变量

  • 改变模块解析方式,在 Vite 文件配置

    resolve: { alias: [ { find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)), }, { // By AI find: /^vant/es/(.+)/style/index/,//正则匹配动态路径replacement:vant/es//, // 正则匹配动态路径 replacement: 'vant/es/1/style/index.css', // 替换为 .css 路径 }, ],},

量化成果

  • FCP 减少 3 秒
  • Lighthouse 性能评分提高 10 分

落地学习路径

由于之前一直使用 CSR 方案开发传统页面,完全没有接触过服务端渲染、性能优化方面的知识,工程化方面的知识也很薄弱;因此在落地过程中遇到了很多问题,这个部分介绍了在落地过程中解决问题的路径以及新接触的知识点

Critical CSS 概念

内联关键 CSS,异步加载非关键 CSS

主流的 Critical CSS 方案

Critters、PostCSS Critical、Penthouse,Critters 包已经不再维护,可以采用 Beasties

在项目中引入没有成功达到效果,需要了解没有效果的原因,看文档的例子都是静态的 html,而 CSR 项目都是浏览器动态渲染的,于是带来一个需要解决的问题:

可以在动态渲染的 HTML 项目中生效吗,如果可以生效,是如何做到的,答案是不行,在查阅资料的过程中看到了一个新的概念预渲染

注意:查阅资料的过程中可能遇到很多干扰信息,比如 Penthouse 采用无头浏览器,而 Critters 没有采用,往深了研究这里的关注点可能会放在无头浏览器,插件如何是如何做到 Critical CSS 的,但这里的关键点其实时解决动态渲染 HTML 的问题,没必要往深了研究,只需要关注解决问题的关键路径

预渲染概念

在构建时生成静态 HTML

预渲染方案

  • vite-ssg:静态站点生成 + Critical CSS 内置方案
  • vite-plugin-ssr:服务端渲染方案
  • vitepress
  • vuepress
  • nuxt

结合项目现状,对比下来选择了成本最低的 vite-ssg 方案

vite-ssg 包引入

引入后项目直接报错,报无法执行 css 文件的错,并且有一个模块化冲突的警告;由于对模块化相关知识、以及服务端渲染相关知识点的缺失,解决这个问题走了很大的弯路

 CommonJS VS ESM

由于看到这个警告好几次了,随即决定先深入了解以下 CommonJS 以及 ESM 模块,CommonJS VS ESM 的文档总结,通过查阅相关资料,对浏览器以及 Node 运行环境有了深入的理解,于是有了一个初步的推测,之所以会报 CSS 相关的错误是因为 Node 在运行相关的代码?

新起项目引入 vite-ssg

新起 vite 项目,直接引入 vite-ssg 项目还是报错,看提示是找不到 window,先不解决了,直接找一个 vite-ssg 引入成功的项目吧

github 寻找成功引入 vite-ssg 的例子

通过这个例子,学到了很多解决问题的关键点,以下是一些关键点:

  • md 文件可以直接转换成 html 文件
  • vite-ssg 项目可以使用 css 样式,如果直接引入 css 样式是可以的,但是在单独的 JS 模块中引入 css 是会报相同的错的

在这里想到了两个大的解决问题的方向:

  1. 改变引入 css 的方式 
  2. 让 vite-ssg 执行不报错 

针对 1 可以直接手动引入 css 或者采用自动化的方案,自动化方案可以从安装依赖环节、自动引入 resolver、Vite 插件直接改写代码

针对 2 需要去了解 vite-ssg 是否提供了响应的配置,深入地需要去了解 vite-ssg 是如何处理 css 文件的,它是如何静态生成 html 文件的

其实通过手动引入 css 样式的方式是可以解决问题的,但是考虑到项目的页面比较多,挨着去手动导入样式比较麻烦,也不好维护,因此需要自动化的解决方案

先在项目中引入 vant 样式吧,然后去实践具体的自动化方案

在实践自动化方案的过程中没有在网上找到合适的插件

vite-ssg 配置项深入解读

没有什么有用的可以控制 css 执行或转换的配置项,不过了解到了模拟浏览器全局变量的核心配置项:mode

VantResolver 理解

提供 type + resolve 方法,能够实现自动引入的核心代码为:

if (name.startsWith('Van')) {  const partialName = name.slice(3)  if (!options.exclude?.includes(partialName)) {    return {      name: partialName,      from: `vant/${moduleType}`,      sideEffects: getSideEffects(kebabCase(partialName), options),    }  }} // 在这里首次通过改变源码 sideEffects 的方式,完成了 Vant css 的自动化解决方案,不过这不是一个好的解决方案

Vite plugin 插件机制(如何运行,包含哪些)

学习到 Vite 打包的流程,以及 Vite 插件的执行机制,以及 Vite 提供额钩子函数,包括 resolveId、load、transform 等

通过对 resolveId 的深度理解与实践,深入理解了构建过程的模块路径解析阶段,最终采用 resolve 配置的方式解决样式引入报错的问题

这里遇到了 load、transform 改写代码不生效的问题

vite-ssg 实现

秉着对静态文件如何生成的好奇,以及寻找更好的解决方案的目的,找到 load、transform 改写代码不生效的原因,深入阅读了 vite-ssg 的源码,在这个过程中了解到,vite-ssg 其实也没有做什么意想不到的事情,整体划分下来也就散步:构建客户端渲染文件 ——> 构建服务端渲染文件 ——>执行服务端渲染生成的文件 ——>以客户端渲染生成的 html 文件为入口,生成需要静态生成的页面,并且添加相应的脚本

服务端渲染深入理解

在看 vite-ssg 源码的过程中,对服务端渲染流程及代码实现有了深入的理解,包括 VueRouter 在 SSR 项目中的应用;客户端与服务端会生成两套代码,客户端的代码用于激活操作,只是纯静态的网站只需要一套代码

css 更好的解决方案

阅读了 vite-ssg 源码、理解了 VueRouter 在服务端的行为,了解了 manifest.json 文件,了解了在生成用于服务端渲染的 JS 过程中会忽略 CSS、图片等静态资源后没有找到更合适的解决方案,通过 manifest.json 文件准确定位到具体路由的代码,最终采用了比实现步骤中更好的解决方案:客户端打包时才自动引入 CSS

const importStyle = !ssrBuild

AutoImport({  imports: ['vue', 'vue-router', 'pinia'],  resolvers: [VantResolver({ importStyle })],  dts: '.typings/auto-import.d.ts',  eslintrc: {    enabled: true,    filepath: '.typings/.eslintrc-auto-import.json',  },}),Components({  resolvers: [    VantResolver({ importStyle }),    IconsResolver({      customCollections: ['custom'], // 自动引入图标 对应的组件名:ICustomSvgname    }),  ],  dts: '.typings/components.d.ts',  globs: [    'src/components/*.{vue,tsx}',    'src/components/*/index.{vue,tsx}',  ], // 只把 components/xxx.vue components/floder/index.vue 作为全局组件}),

vite 打包机制

这里其实还有一个遗留的问题就是例子项目中非 main.ts 文件引入的样式会被 JS 异步加载,因此 Critical CSS 不会生效,而待优化项目默认会同步引入首屏用到的样式,没有找到原因,不过这暂时不影响 Critical CSS 在优化项目中落地的主任务,将它作为一个代办项吧

技术约束

动态生成 DOM 结构

待办项

为什么例子项目的 Critical CSS 样式没有生效?

项目使用 vue-router/auto-routes 自动生成的路由,每个页面都没有同步打包

QA

  1. 解决引入 Vant 样式报错的核心发现是什么?
  • 在成功引入 vite-ssg 的项目中导入引入 css 模块的模块会报错