React 页面加载速度优化(非SSR)


React 应用页面首次加载通常比后端渲染静态页面慢,这对用户体验有着极大的负面影响,尤其是移动端。服务器端渲染(SSR)是非常有效的手段,但实现成本过高,不止要调整技术架构,也会增加服务器负载。那么在不使用 SSR 的前提下,如何优化 React 应用的首屏加载速度呢?

加载过程分析

使用 Firefox 开发者工具网络面板对页面加载过程中的网络流量进行监控,是最常用的分析方法。

注意右侧的进度瀑布图有两根线条,一根红色,一根蓝色:

  • 蓝色是 DOMContentLoaded 事件触发的时间点
  • 红色是 load 事件触发的时间点

但是对于 React 应用而言,以上两个事件并不标志着页面内容已加载完成。React 应用通常会需要请求一些 JSON 接口(比如商品信息),然后再加载 UI 组件(比如商品列表)。实际加载时间,我们可以通过 performance.now() 接口在组件实际挂载(componentDidMount 或 useEffect)时获取:

import React, { useEffect, useState } from 'react';

function App() {
  const [products, setProducts] = useState([]);
  useEffect(() => {
    fetch('api/products').then(res => res.json()).then(setProducts);
  }, []);
  if (products?.length > 0) {
    return <ProductList products={products} />
  } else {
    return <div>Loading...</div>;
  }
}

function ProductList({ products }) {
  useEffect(() => {
    console.log('ProductList loaded in ' + performance.now()/1000 + 's');
  }, []);
  return products.map(({name}) => <div>{name}</div>);
}

本地的测试分析不能反映实际用户的加载速度。建议将加载时长结果上报到服务器,进行统计分析,制作按日/小时的趋势图,以及按加载秒数的分布图。统计结果将有助于判断后续的优化工作的效用有多大。

数据加载优化

首屏展示依赖的数据,后端直接注入到 HTML 中,从而减少网络请求数量。

<script>
  window.user = {{ JSON.stringify(user) }};
  window.productInfo = {{ JSON.stringify(productInfo) }};
  window.translations = {{ JSON.stringify(translations) }};
</script>
<script src="path/to/js"></script>

要小心控制注入 HTML 中的数据数量,过大的 HTML 可能会导致页面加载失败的概率增加。首屏不展示的数据,或者不重要的数据要排除:Select 的数据源,弹窗中用到的数据,通知数据等。翻译数据最好按首屏需要做一次切分,且只透出当前用户语言。

从实践经验来看,这一项措施,对加载速度提升最为明显。

图标加载优化

icon font 是图标加载方式的主流,但加载字体文件会多发出一个请求。且字体请求通常是在 CSS 中触发,会阻塞页面加载。另外,字体文件很难根据首屏内容进行切分,不够灵活。某些大型应用,图标高达上百个,加载数 MB 的图标字体会让体验变得很差。

更好的方案是用 SVG Sprite 直接嵌入 HTML 中,或用 Inline SVG 直接嵌入 JS 中,减少一次网络请求,同时只加载首屏需要的图标。

打包尺寸优化

首先,建议所有使用 Webpack 4 的项目迁移到基于 Webpack 5 或者 Rollup 的构建工具,以使用 ES6 Module 的特性进行 Tree Shaking,去掉未被使用的部分,缩减产物尺寸。

对于 lodash 这种非 esm 包,建议用 lodash-es 替代或者采用分量引用:

// Bad
import _ from 'lodash';

// Good
import get from 'lodash/get';

最后,使用 Webpack Bundle Analyzer 插件分析产物尺寸,识别对产物体积影响较大的包。

  1. 存在多版本的包,考虑升级依赖版本进行优化。
  2. 尺寸过大的包,考虑轻量级替代品,比如 moment -> date-fns/day.js

代码分割

注意:不支持 IE,Edge < 16,Firefox < 60,Chrome < 61 和 Safari < 11

常见的构建工具都支持以下语法进行代码分割:

import React from 'react';
import Header from './Header';
const Description = React.lazy(() => import('./Description'));
const Comments = React.lazy(() => import('./Comments'));
const FeedbackDialog = React.lazy(() => import('./FeedbackDialog'));
const Footer = React.lazy(() => import('./Footer'));

function ProductDetailPage() {
  return <div>
    <Header/>
    <Description/>
    <Comments/>
    <FeedbackDialog/>
    <Footer/>
  </div>
}

对于首屏需要展示的组件,Header,我们用传统的引用方式打包,在页面加载时直接加载其 JS 和 CSS。而对于不会首屏展示的组件,用 ES6 模块动态加载。这样做之后,首屏加载的 CSS 和 JS 尺寸会缩小,从而加快首屏展示。

参考 React 官方文档

但实际项目中,构成项目产物的大部分来自第三方包,比如 React,Moment 和 Ant Design 这样的库,也就是 vendor chunk。如果你的业务代码产物 app chunk 在 gzip 压缩之后不超过 200KB,做代码分割的意义就不大了。

总结

在实际工作中,由于技术架构限制或是资源限制无法使用 SSR,我们还是有很多手段可以去快速优化 React 应用加载速度。

本文没有涉及 HTTP2,缓存策略,CDN 等优化传输速度的方法,因为这些是 DevOps 考虑的范畴,在大的 IT 公司中,都已经做得比较到位了,前端开发者不需要亲历亲为。


发表评论

您的电子邮箱地址不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据