用户普遍反馈项目首屏加载比较满,所以我做了完整的一次性能分析,定位到几个关键的瓶颈;
1.问题主要在js bundle体积过大(3.2mb) 和首屏并发请求过多(12个);
2.bundle问题做了代码分割和懒加载,路由改成动态的import、三方库按需引入。
针对接口就是梳理了下数据依赖,把12个借口合并成2个关键接口。
所以首屏加载时间从4.2秒优化到1.8秒,fcp指标提升了60%,效果显著
如何分析出的这些瓶颈的?
1.performance:
浏览器吧检查调出来有一个performance那一栏,点击录制,然后操作页面,再stop停止录制,就会出现一个火焰图。
发现主线程被大量的js解析任务阻塞(黄色部分占据很大一个比例),同时用Lighthouse跑分,量化出FCP和LCP指标确实比较低。
2.构建分析:
用webpack-buldle-analyzer对打包产物进行可视化分析,一下就可以看出moment.js这种库占了韩大的体积,就改用更轻量的days.js
使用:yarn add --dev webpack-bundle-analyzer
通过 Webpack 配置插件(推荐)
在你的 webpack.config.js
(或者 webpack.prod.js
等构建配置文件)中引入并使用该插件:
示例代码:
const BundleAnalyzerPlugin =
require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
// ...你的其他 webpack 配置
plugins: [ // 添加这个插件(不传参数默认会在构建后自动打开分析页面)
new BundleAnalyzerPlugin()
] };
🧠 如何操作:
-
∙ 运行 webpack 构建后,该插件会启动一个本地 HTTP 服务器(通常是 http://localhost:8888)
-
∙ 然后自动打开浏览器,展示打包结果的可视化分析页面
-
∙ 页面中以矩形树图(Treemap)的形式展示各个模块的大小与依赖关系
如何解决细节
组件方面:
对于一些体积比较大,但是不是一进入页面理解展示的组件 通过动态import()方式进行分割;
代码分离
配置方面:
在webpack配置层面,使用了splitChunks,将常用的第三方库和公共模块抽离成独立的chunk,以便浏览器长期缓存'
将代码分离到不同的bundle
中,之后我们可以按需加载,或者并行加载这些文件
默认情况下,所有的JavaScript
代码(业务代码、第三方依赖、暂时没有用到的模块)在首页全部都加载,就会影响首页的加载速度
代码分离可以分出出更小的bundle
,以及控制资源加载优先级,提供代码的加载性能
这里通过splitChunksPlugin
来实现,该插件webpack
已经默认安装和集成,只需要配置即可
默认配置中,chunks仅仅针对于异步(async)请求,我们可以设置为initial或者all
module.exports = {
...
optimization:{
splitChunks:{
chunks:"all"
}
}
}
splitChunks
主要属性有如下:
- Chunks,对同步代码还是异步代码进行处理
- minSize: 拆分包的大小, 至少为minSize,如何包的大小不超过minSize,这个包不会拆分
- maxSize: 将大于maxSize的包,拆分为不小于minSize的包
- minChunks:被引入的次数,默认是1
如何保证代码分割后的用户体验
1.我加了一个loading等待的效果,缓解用户的等待焦虑;或者suspense的fallback设计一个骨架屏
import React, { Suspense, lazy } from 'react';
// 动态导入组件 => 会生成单独的 chunk
const HomePage = lazy(() => import('./pages/HomePage'));
function App() {
return (
<Suspense fallback={<div>🌀 页面加载中,请稍候...</div>}>
<HomePage />
</Suspense>
);
}
2.我还弄了一个预加载,就是在用户鼠标hover导航菜单时候,以前与加载对应页面的chunk,让页面条环更快
实现思路:
-
∙ 不是直接调用
import('./Page')
(这会立即加载),而是将动态导入函数保存下来,在 hover 事件里 手动调用它。 -
∙ React.lazy 的
import()
是自动执行的,所以我们需要 自行封装动态 import,以拿到 Promise 函数,实现"按需手动触发"。
// 不使用 React.lazy,而是自己封装动态加载函数
const getHomePage = () => import('./pages/HomePage');
// 在导航菜单 hover 时预加载
function Navigation() {
const handleMouseEnter = () => {
// hover 时触发 chunk 加载
getHomePage(); // 这里会开始下载对应 chunk
};
return (
<nav>
<span onMouseEnter={handleMouseEnter}>首页</span>
</nav>
);
}
// 真正渲染组件时仍然用 React.lazy + Suspense
const HomePage = React.lazy(getHomePage);
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<HomePage />
</Suspense>
);
}
3.用错误边界包裹了动态加载的组件,这样即使整个chunk因为网络问题加载失败,整个应用也不会白屏,给他展示一个toast提示。
除了这些 还有什么其他可以首屏优化的,协同团队做了基础设施层的优化;
1.将所有静态资源部书到cdn的边缘节点,开启http/2协议和gzip压缩
2.和后端同学一起,为核心api增加额redis缓存,并对曼查询的数据库表增加了索引,给接口响应时间从800ms降低到200ms
3。建立监控体系:部署了前端性能监控系统,能够持续追踪core web vitals等核心指标,让优化不再是一次性行为,而是一个长期迭代的体系