Skip to content

8.3 首屏渲染优化

首屏渲染优化是前端性能优化中极其重要的一环,首屏渲染速度与用户的留存率直接挂钩。在传统多页面应用中,一个页面对应一个 HTML 文件,首屏优化只需要优化一个页面的代码就可以了;但是在单页面应用中,HTML 只有一个,JavaScript 控制模块的展示,因此优化首屏渲染速度要从整个应用出发。

本节我们以单页面应用为例,结合构建工具配置,解释下如何优化首屏渲染。

8.3.1 首屏变慢的原因

单页面应用通过 JavaScript 管理路由和模块。当应用规模变大时,一个难以避免的场景是模块体积变大,同时复杂的依赖会使更多的模块需要被下载和执行,这样会严重拖慢首页加载时间,用户看到的白屏时间也就会更久。所以资源的“大”和“多”是首屏变慢的主要原因。

如何做首屏渲染优化?核心思路就是解决“大”和“多”多问题。既然模块体积大,哪我们就想办法拆分,将首页用不到的代码逻辑或其他依赖拆分到单独的包中,这样首页的模块体积就会立刻变小。同时也可在服务端将模块体积压缩,让下载速度更快,双重优化下会大幅提高首屏渲染速度。

至于模块数量多的问题,首先在代码层面尽量不要全局引入模块,减少全局公共模块的数量。其次业务中需要的各类模块,我们可以让和页面展示有关的模块优先加载,甚至可以延迟加载交互模块,让页面先出来再说!

8.3.2 路由懒加载

懒加载是一个非常成熟切效果极好的优化手段,在解决首页问题时,请首先尝试路由懒加载。

路由懒加载是指:当访问某一个路由时,浏览器只加载当前路由对应的模块,其他不需要在当前路由显示或运行的模块暂不加载;当切换路由时,浏览器再去加载新路由对应的模块,这样就能大大减小首次加载的体积,正好符合“需要什么就加载什么”的分布加载思想。

路由懒加载如何实现呢?不同的框架有不同的实现。在 Vue 中,我们通过 import() 函数实现,如下:

js
const Test = () => import("@/pages/Test.vue");
const router = createRouter({
  routes: [
    {
      path: "/test",
      component: Test,
    },
  ],
});

在 React 中同样使用 import() 函数,但时要多一层 React.lazy() 函数包裹:

js
const Test = React.lazy(()=> import('@/pages/Test.vue'))
<HashRouter>
  <Route path="/test" exact={exact} component={Test} />
</HashRouter>

但不管哪个框架,懒加载功能都是由 ES6 提供的 import() 函数提供的。在使用 Vite 构建项目时,只要遇到使用 import() 函数加载的模块,Vite 会自动将其打包一个单独的文件,这为模块的懒加载提供了条件。

8.3.3 Gzip 压缩

前端项目打包后会生成静态资源文件,对于一些业务复杂的模块,构建优化之后文件的体积依然很大,那么还可以进一步优化吗?答案是肯定的,我们还可以压缩文件。压缩是在构建优化之后的另一层资源优化,压缩会减小文件体积,自然会提升加载速度。

网络资源压缩最高效的方法是 Gzip 压缩,它可将模块体积压缩 50%。Gzip 压缩可以在服务端压缩,也可以在客户端压缩,它们的区别如下:

  • 服务端压缩:当服务端收到资源请求后,会实时压缩资源并返回客户端。
  • 客户端压缩:客户端编译生成 .gzip 文件并上传,服务器收到请求后直接返回该文件。

如果在服务端压缩,当用户首次访问网站时,服务端会消耗一定的时间和 CPU 去压缩资源,压缩后再返回客户端,这样会影响首屏速度,也会加大服务端负载。如果在客户端压缩并上传至服务器,服务端开启 Gzip 后可以直接返回该文件,不需要服务器进行压缩,显然这种方案性能更好。

在 Vite 中,打包生成压缩文件非常简单,只需要一个插件 vite-plugin-compression 即可。首先安装该插件,然后在 vite.config.ts 中配置如下:

js
// vite.config.ts
import { defineConfig } from "vite";
import viteCompression from "vite-plugin-compression";

export default defineConfig({
  plugins: [viteCompression()],
});

接着我们执行打包命令,就可以看到构建后输出的每个文件都有一个对应的 .gz 压缩文件,如下图所示:

将构建后的文件上传到服务器,但此时服务器还没有开启 Gzip,因此不会加载你的文件。我们在 nginx 中配置开启 Gzip,配置如下:

sh
server {
  ...
  gzip on; # 开启 Gzip
  gzip_buffers 16 8k;
  gzip_comp_level 6;
  gzip_vary on;
  gzip_types text/plain text/css application/json application/javascript text/javascript; # 对于哪些类型的文件开启 Gzip
  gzip_min_length 1k;
}

使用 “nginx -s reload” 重载配置,然后在浏览器中刷新页面,此时压缩多半已经生效了。查看网络请求中的响应头部分,如果有一下标识,说明 Gzip 开启生效。

8.3.4 服务端渲染

在单页面应用中,几乎所有项目都遵循“客户端渲染”的模式。该模式下服务端把一个 HTML 文件发给浏览器,内容非常简洁,只包含一个空的 div 标签和一个链接文件的 script 标签。以 Vue3 为例,HTML 文件的 body 标签内容如下:

html
<body>
  <div id="root"></div>
  <script type="module" src="index.js"></script>
</body>

上面代码中,body 标签下无任何可见元素。当浏览器解析到 script 标签时,链接对应的 JS 文件开始下载并执行,执行过程中会异步获取其他的文件或数据,最终生成 DOM 结构将其填充到页面中。当 JavaScript 执行完毕,页面内容才会被完整地渲染出来。

在客户端渲染模式下,整个页面的 DOM 结构需要执行 JavaScript 才能生成;如果项目规模大,JavaScript 执行的时间会更长,首页渲染的也就更慢。

从渲染模式上解决这个问题,我们有两种方案:预渲染和服务端渲染。

  1. 预渲染(SSG)

预渲染主要依赖打包工具实现,具体的实施方法是:将组件代码经过处理输出一个包含最终 DOM 结构的 HTML 静态文件。当页面加载时,我们直接渲染这个文件,不需要在服务端做任何处理。

一般情况下,预渲染会按照既定的目录结构将项目拆分为多个 HTML,部署后直接访问不同的文件路径即可。但是请注意:因为是一次性生成静态页面,所以预渲染不适合开发动态网页。比如新闻详情页,数据不同页面内容就不同,这种情况就不适合使用预渲染。

预渲染生成了包含 DOM 结构的 HTML 页面,实现起来更简单,且不需要依托服务器,最适用构建静态站点。缺点是不支持动态页面,动态页面则必须使用服务端渲染构建。

  1. 服务端渲染(SSR)

什么是服务端渲染呢?就是让整个页面的 DOM 结构在服务端全部处理好,然后返回给客户端,客户端拿到就可以直接渲染,省去了执行 JavaScript 的时间,这样首页的渲染速度自然会很快。

既然是服务端渲染,那么必然要有一个负责渲染页面的服务,该服务多数由 Node.js 实现。在服务端渲染的架构中,包含 client 和 server 两部分代码:前者是前端代码,后者是负责渲染的 Node.js 代码。项目打包之后代码依然是两份,我们需要将它们部署到线上。

不同的是,SPA 应用可以直接以静态资源的方式部署,而 SSR 应用则必须以 Node.js 的方式部署。SSR 的页面路由由 Node.js 接管,负责接收请求并渲染页面,因此它叫做服务端渲染。

在 Vite 项目中使用插件 vite-plugin-ssr 可以快速搭建 SSR 应用,首先用该脚手架创建项目:

sh
$ npm init vite-plugin-ssr@latest

创建之后打开项目,可以看到目录下有 pages 文件夹,下面的每个目录代表一个页面。该插件同时支持预渲染和服务端渲染,默认模式为服务端渲染。如果要打包预渲染,只需修改配置如下:

js
// vite.config.ts
import { ssr } from "vite-plugin-ssr/plugin";
export default {
  plugins: [ssr({ prerender: true })],
};

服务端渲染虽然好,但是改造成本比较大。一般情况下如果项目有 SEO 和首页性能的强需求,可以考虑使用服务端渲染;如果只是优化首页渲染速度的话,优先尝试其他方案。