事情是这样的,上个月我接了个后台管理系统的重构活儿,React + TypeScript + Ant Design Pro,听起来挺常规对吧?结果一上线就被运营大姐吐槽“点个按钮卡三秒”,我当场血压就上来了。拿Chrome的Lighthouse一跑,Performance分数42,首屏加载时间6.8秒,好家伙,这数据发到技术群里能被笑一年。
先说说最离谱的问题。这个项目用了大量的ECharts图表,而且每个图表都是在组件挂载时直接new ECharts实例。我打开Network面板一看,echarts.min.js 1.2MB全量加载,再加上各个图表配置项里引用的地图JSON数据,光图表相关就占了2.8MB。更蠢的是,首页其实只有两个折线图,但代码里import了所有图表类型。解决方案其实很粗暴:按需加载。我把import * as echarts from ‘echarts’改成了import { init, LineChart, TooltipComponent } from ‘echarts/core’。这一步直接从1.2MB砍到320KB。地图数据也做了懒加载,只在用户打开“地理分布”Tab时才fetch对应的GeoJSON。改完之后首屏JS体积从4.6MB降到2.1MB,效果立竿见影。
然后是图片资源的坑。项目里有个轮播图组件,用的是png格式,每张图2-3MB,一共8张。我寻思着这玩意又不是摄影作品,直接全转成WebP,用Sharp跑了个批处理脚本。转完之后每张图300-400KB,质量肉眼看不出区别。但有个细节要注意:Safari不支持WebP,所以我在nginx配置里加了判断逻辑,根据Accept请求头里的image/webp来决定返回什么格式。具体配置长这样:
“`
location ~* \.(jpg|jpeg|png)$ {
add_header Vary Accept;
if ($http_accept ~* “image/webp”) {
rewrite (.*)\.(jpg|jpeg|png)$ $1.webp break;
}
}
“`
这里有个坑,rewrite后面一定要加break,不然会循环重写导致500。
接下来是React层面的优化。我翻了下组件树,发现一个列表页面每行都有一个下拉菜单,每个菜单里又有十几个选项。这些下拉组件都是直接写在循环里的,每次数据刷新都会全部重新渲染。用React DevTools的Profiler跑了一下,单个列表项渲染耗时12ms,200条数据就是2.4秒的阻塞。解决办法是把下拉菜单抽成独立的Memo组件,并且用useMemo缓存选项列表。具体代码大概是:
“`tsx
const MemoizedDropdown = React.memo(({ options, value, onChange }) => {
const memoizedOptions = useMemo(() =>
options.map(opt => ({ label: opt.name, value: opt.id })),
[options]
);
return ;
});
“`
注意React.memo的第二个参数是areEqual函数,默认做浅比较,如果你的options每次都是新对象,那就得自己写比较逻辑,或者用useMemo兜底。我在这卡了半小时,因为options是每次从父组件传进来的新数组,memo根本没生效。
还有个大头是路由懒加载。这个项目用了react-router v6,但所有页面组件都在一个bundle里。我改成用React.lazy + Suspense按路由拆分。但有个坑是Ant Design Pro的ProLayout组件里如果引用了lazy的组件,第一次加载时会闪一下空白。解决方案是在Suspense的fallback里放一个和页面高度一致的骨架屏,而不是单纯转圈圈。我用的是antd的Skeleton组件,设置active和paragraph={{ rows: 8 }},视觉上平滑很多。
最后说一个容易忽略的点:第三方脚本的加载时机。项目集成了百度统计和腾讯云监控的SDK,这两个脚本都是同步加载的,阻塞了DOM渲染。我改成用async加载,但百度统计的脚本里有document.write,用了async会报错。查了文档,解决方案是在script标签上同时加defer和async,或者用动态创建script节点的方式在onload之后注入。我选了后者:
“`js
const loadScript = (src) => {
return new Promise((resolve) => {
const script = document.createElement(‘script’);
script.src = src;
script.onload = resolve;
script.async = true;
document.body.appendChild(script);
});
};
window.addEventListener(‘load’, () => {
loadScript(‘https://hm.baidu.com/hm.js?xxx’);
});
“`
这样统计脚本不会影响首屏,而且页面加载完成后才会发起请求。
优化完再跑Lighthouse,Performance分数到了89,首屏加载时间1.8秒。运营大姐没再吐槽,但产品经理开始加新需求了。总结一下踩的坑:第一,别信“先上线再优化”这种鬼话,等代码堆到一万行再回头改,成本高到你想删库跑路。第二,Chrome DevTools的Coverage面板很好用,能看到哪些代码从来没执行过,我靠它删掉了三个从来没被调用的图表组件。第三,图片压缩一定要在CI/CD流程里自动化,否则下次有人直接拖个PSD里的原图进来,优化就白做了。