1. 写在前面:为什么今天还要翻这份“老古董”清单
聊到前端性能优化,绕不开一份“老古董”——雅虎35条军规。这份由雅虎前端团队在多年前发布的性能优化清单,至今依然是面试高频题、架构评审对照表、甚至不少团队新人培训的第一份教材。我见过很多开发者一听到“35条”就头大,觉得条目太多记不住;也有人觉得时代变了,HTTP/2、现代框架改变了规则,老清单早该扔进垃圾桶。但以我实际操作的经验来看,这份清单的核心价值不在于每一条是否还适用,而在于它把性能优化从“玄学”变回了“清单式工程”。你不需要灵感,只需要逐条排查,就能找到绝大多数页面变慢的原因。
说得直接一点:前端性能优化不是炫技,是排查与取舍的系统工程。雅虎35条军规恰好提供了一张经过实战检验的排查表。它背后归纳的其实是七大类能力:服务器配置、HTML与CSS书写、JavaScript执行、图片处理、Cookie管理、移动端适配、以及内容资源本身。这几类几乎覆盖了从用户输入URL到页面完全渲染的全链路。
这篇文章我不会按“第1条到第35条”机械翻译,而是把规则重新组织成几个核心模块,讲清楚每条规则背后的原理、适用场景、以及现代技术栈下哪些规则需要“升级版本”,哪些规则依然“打死不能违反”。适合谁看?三种人:刚入行的前端新手,需要建立性能优化全局观念;写了好几年业务代码、但没系统排查过性能瓶颈的开发者;以及正在做技术方案评审、需要一份可落地检查清单的技术负责人。
提示:文中涉及的具体配置和代码以常见实践为准,不同业务场景需要根据实际压测数据调整,不要无脑照抄。
2. 服务器与网络链路:减少一切不必要的往返
这一部分统管用户浏览器与服务器之间的所有通信,核心思想就一句话:能少请求就少请求,能缓存就缓存,能压缩就压缩。网络延迟是用户感知最明显、也是最难通过前端代码优化的部分,所以必须先从这里入手。
2.1 减少HTTP请求:把“多次出门采购”改成“一次大采购”
军规第一条就是减少HTTP请求次数。原理很好理解:HTTP请求是有成本的,每一次请求都需要经过DNS解析、TCP握手、TLS协商、发送请求头、等待响应、下载资源这一整套流程,在移动端弱网环境下,这个成本还会被成倍放大。减少请求次数最直接的方式有三种:合并CSS/JS文件、使用雪碧图(CSS Sprites)、内联小体积图片为Base64。在现代工程化体系下,合并文件这件事已经被打包工具自动完成了——Webpack、Vite、Rollup都会根据配置把模块合并成尽可能少的几个chunk。但要注意,合并不是无脑合并,把一个5MB的JS全塞进首屏,还不如拆成首屏必需和非必需两部分。这里的原则是:首屏关键资源合并压缩,非关键资源异步加载,能延迟加载的绝不提前加载。
以我实际做过的一个后台管理系统为例,最初首屏加载了47个JS文件、23个CSS文件,FCP(First Contentful Paint)在模拟3G网络下达到6.8秒。后来做了三件事:用打包工具把第三方库单独拆成vendor包,业务代码按路由懒加载,公共样式合并为一个文件。最终首屏请求数降到11个,FCP降到2.1秒。这里的经验是:不要追求“绝对最少的请求数”,而是追求“首屏关键路径上最少的请求数”。
2.2 使用CDN:把内容搬到用户家门口
雅虎军规里说的CDN,用生活化类比就是“把超市开到你家楼下”。CDN(内容分发网络)把静态资源缓存到离用户物理距离更近的边缘节点上,用户请求时直接从边缘节点返回,省去了跨地域的长距离传输。这个优化对首屏的收益往往比代码层面的优化更明显,尤其是静态资源占比高的页面。
实际使用中有几个容易被忽略的点。第一,CDN上缓存的是静态资源,一定要给文件名带上内容哈希,这样文件内容变更时文件名也会变,CDN就会回源拉新文件,而不是继续吐旧缓存。第二,要设置合理的缓存时间,一般图片、CSS、JS这类不可变资源可以设置30天以上长缓存。第三,静态资源一定要和动态接口分离,最好部署在不同域名下,因为静态资源不会携带业务Cookie,减小请求头体积,还能避免CDN缓存被Cookie污染。我见过不少项目把所有请求都打在同个域名下,结果CDN缓存命中率极低,所谓的“CDN加速”形同虚设。
2.3 启用gzip/压缩:体积下来了,速度自然就上去了
文本类资源(HTML、CSS、JS、JSON等)的压缩收益非常可观,通常能减少60%~80%的体积。gzip是应用最广泛的HTTP压缩方案,现在的服务器基本都支持动态压缩或预压缩。配置起来也不复杂,Nginx里几行配置就能开启:
gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss image/svg+xml; gzip_min_length 1024; gzip_comp_level 5;有几个参数值得注意。gzip_min_length 1024的意思是小于1KB的文件不压缩,因为压缩头部开销可能比节省的还多,反而得不偿失;gzip_comp_level不是越高越好,5~6是性价比最高的区间,再往上压缩率提升非常有限但CPU消耗明显增加。另外,现在新流行的Brotli压缩算法比gzip压缩率还要高10%~20%,主流浏览器基本都支持,Nginx或云厂商CDN也大多支持,建议优先开启。
2.4 合理设置缓存与ETag:让浏览器“少跑几趟”
HTTP缓存是容易被忽略但效果惊人的优化手段。缓存的核心就是服务器通过响应头告诉浏览器“这个文件你可以多久不用再来问我”。最常见的两个响应头是Cache-Control和Expires,前者是HTTP/1.1标准,优先级更高;后者是HTTP/1.0老产物,现在基本可以不看了。现代项目里推荐这么设置:
- 带内容哈希的静态资源:
Cache-Control: public, max-age=31536000, immutable - HTML文件:
Cache-Control: no-cache(意思是浏览器每次都会回服务器问一下文件变了没有) - 接口请求:根据业务需要决定,但一般不要用长缓存
ETag是另一个容易被忽略的字段。简单说,ETag就是服务器给文件加的一个“版本标签”,文件内容变了标签就变。当浏览器再次请求时,会带上If-None-Match头,服务器比对ETag后发现文件没变,就返回304状态码,不返回文件内容,从而省下重新下载的流量。注意,ETag不是替代Cache-Control的方案,而是配合使用:Cache-Control负责告诉浏览器“什么时候该重新验证”,ETag负责验证“文件到底变了没有”。
3. 页面结构优化:把关键内容放在最显眼的位置
这部分对应的是HTML和CSS层面的优化,核心逻辑是“浏览器解析HTML和CSS的方式决定了页面出现的速度”。理解这些规则,你才能真正理解为什么“CSS放头部、JS放底部”这种老话依然成立。
3.1 将CSS放在顶部:让页面“先穿好衣服再见人”
浏览器渲染页面的过程不是等所有资源下载完才开始的,而是边解析HTML边构建渲染树。如果CSS放在HTML结构后面或动态注入太晚,浏览器在CSS还没加载完成之前,无法确定元素该是什么样子,就只能先渲染无样式内容(FOUC,Flash of Unstyled Content),然后又突然“变装”成有样式的状态,用户会明显感到闪动。
所以CSS必须放在<head>里,用<link>标签同步加载。现代工程化框架里,组件级CSS通常被打包工具提取合并后自动注入到head中,不太需要手动操心。但如果你在做一些简单的多页应用,或需要手写页面模板,请务必保持“head里引入CSS,body结尾前引入JS”的习惯。还有一个提升点是:首屏不需要的CSS(比如弹窗样式、折叠区域样式)可以拆出来异步加载,用preload或动态注入<link rel="stylesheet">的方式延迟加载,避免首屏CSS文件过大。
3.2 将JavaScript放在底部:别让脚本挡住页面“发育”
HTML解析是逐个标签往下走的,遇到<script>标签时必须停下来:下载脚本、解析执行脚本,然后才能继续解析后面的HTML。如果把JS放在head中,浏览器在拿到全部JS并执行完毕之前,页面主体内容根本不会渲染出来。这就像做饭时把火关掉等菜切完再开火,整个流程被卡住了。
把<script>标签放到</body>之前,可以保证页面主体HTML先解析完成、首屏先呈现,然后脚本再逐步执行。现代更好的方案是使用<script defer>或<script async>属性。区别在于:defer是脚本在HTML解析完后按顺序执行,async是下载完成后立刻执行、不保证顺序。推荐在模块化脚本上优先用defer,它既不会阻塞解析,又能保证依赖顺序。async适合完全独立的脚本,比如埋点统计、第三方分析SDK。
3.3 少用CSS表达式与避免重排重绘:让页面“省着点用”
CSS表达式是IE时代的老顽固,因为表达式会在页面滚动、鼠标移动等大量场景下被反复求值,极其消耗性能。这条规则放到今天可以扩展为更普遍的两条:避免高消耗的CSS选择器和减少强制重排(Reflow)与重绘(Repaint)。
CSS选择器的匹配是从右往左进行的,.container .content .item这种深层选择器匹配效率远低于.item这种单类选择器。但现代浏览器的选择器匹配效率已经非常高,普通页面几乎感知不到差异。真正的性能杀手是布局抖动(Layout Thrashing),就是JS反复读写导致浏览器被迫反复重新布局。例如在循环里先读element.offsetHeight,紧接着又修改element.style.height,就会引发连续的重排。正确做法是先把需要读的布局值缓存起来,批量修改样式,或使用requestAnimationFrame统一在下一帧修改。下面给个对比示例:
// 循环内强制重排的写法(不推荐) for (let i = 0; i < items.length; i++) { const height = items[i].offsetHeight; // 读布局 items[i].style.transform = `translateY(${height + 10}px)`; // 写样式 } // 优化后:先批量读取,再批量写入 const heightList = []; for (let i = 0; i < items.length; i++) { heightList.push(items[i].offsetHeight); } for (let i = 0; i < items.length; i++) { items[i].style.transform = `translateY(${heightList[i] + 10}px)`; }3.4 代码压缩与移除重复脚本:给HTML“减减脂”
压缩代码听起来是老生常谈,但直到今天仍有大量线上项目没有启用压缩。压缩不只是去掉空格和换行,还包括变量名缩短、删除无用代码(Tree Shaking)、合并重复的字符串字面量。现代打包工具默认就会做这些事情,但要注意几个坑:
- 压缩前后务必对比文件内容,防止压缩工具误删被字符串引用的类名(如
className="icon-xxx"被Tree Shaking删掉)。 - 第三方库尽量引入压缩版本(
dist/*.min.js),同时结合Tree Shaking只打包用到的模块。 - 移除重复脚本这条规则特别容易出现在老项目改造中。多个页面都引入了同样的jQuery库、同样的公共工具函数,结果打包后出现了两份。用
webpack-bundle-analyzer检查一番,通常能发现不少“看起来没毛病”的重复依赖。
4. JavaScript与DOM操作:不要让脚本拖垮整个页面
JavaScript执行是前端性能消耗的重灾区。雅虎军规中有好几条专门针对JS的规则,它们的核心思想是:减少脚本数量和体积、避免阻塞渲染、减少不必要的DOM操作。这几条在现代单页应用中尤其值得反复品味。
4.1 脚本合并与按需加载:别让用户为用不到的功能“买单”
雅虎军规建议合并多个JS文件为一个大文件,原理还是减少HTTP请求。现代构建工具在“合并”上的做法更精细了:基础库(如React、Vue)单独打成vendor包,利用浏览器缓存让用户切换页面时不用重复下载框架代码;业务代码按路由切分,访问哪个页面就下载哪个页面的代码。核心原则没变,只是从“物理合并成一个文件”变成了“逻辑上分块、加载时按需取用”。
有一个理解上的误区需要纠正:“合并成一个文件”不等于最优解。当你的项目体量到一定程度后,单个JS文件可能超过2MB,这时把它切成若干块、配合preload提前加载关键块,首屏体验反而更好。所以这条规则的正确理解是:减少关键路径上的请求数,而不是一味追求全局请求数最少。以我维护的一个单页应用为例,主包从3.2MB拆成“入口+四个路由块”后,首屏加载体积从3.2MB降到600KB,首屏时间从4.5秒降到1.8秒,这就是“按需”的威力。
4.2 使用事件委托:别让页面每挂一个节点都绑一次监听
事件委托利用了事件冒泡机制:子元素触发的事件会冒泡到父元素,所以只需要在父元素上监听一次,就能处理所有符合条件子元素的事件。这条规则在动态渲染列表的页面中尤其重要——如果用循环给1000个列表项分别绑定点击事件,会产生1000个监听器;而用事件委托,只需要1个监听器。
// 不推荐:给每个列表项单独绑定 document.querySelectorAll('.list-item').forEach((item) => { item.addEventListener('click', handleClick); }); // 推荐:借助事件委托,父元素一个监听搞定 document.querySelector('.list').addEventListener('click', (e) => { const target = e.target.closest('.list-item'); if (target) handleClick(target); });现代框架(如React、Vue)在合成事件层面已经自动做了类似处理,但如果你在维护原生JS项目,或者在事件监听比较密集的场景(如长列表虚拟滚动),掌握事件委托依然能直接降低内存占用和事件处理开销。事件委托的另一个好处是:动态新增的DOM节点不需要重新绑定事件,省掉不少重复代码。
4.3 注意DOM节点数量与性能:别让页面变成“巨型文档树”
DOM节点数量过大会引发三类问题:首次渲染变慢、重排重绘成本增高、内存占用变大。你可以打开DevTools的Performance面板,在页面加载时记录一段性能概况,看看Layout和Recalculate Style两个阶段的耗时——如果它们经常超过100ms,很可能说明DOM树过于庞大。
一条经验法则:单页可视区域内的DOM节点数量尽量控制在合理范围,长列表务必做虚拟滚动。我优化过一个后台报表页面,原本一次性渲染了5000行表格数据,DOM节点数突破30000,滚动时卡顿得没法看。引入虚拟滚动组件后,只渲染可见区域的30行,配合固定行高,滚动性能从“肉眼可见的卡顿”恢复到“丝般顺滑”。你可能觉得虚拟滚动实现起来复杂,但现在很多组件库自带虚拟滚动表格,直接用就行。重点在于:开发时要有“DOM量和首屏性能成反比”的意识,别一口气把数据全塞进DOM里。
4.4 最小化DOM访问次数:缓存变量,减少查询
这条规则在原生JS时代极其重要,但现代框架把大部分DOM操作都隐藏到了虚拟DOM和Diff算法里,开发者很少直接操作DOM。不过,如果你还在写一些插件、工具库、或需要进行精细DOM操作的项目,这条规则依旧管用。核心做法就一句话:反复用到的DOM节点,先存到变量里再使用,避免每次操作都重新querySelector查询。
// 不推荐:每次循环都查询DOM for (let i = 0; i < 10; i++) { document.querySelector('.item').style.left = i + 'px'; } // 推荐:先缓存DOM引用,再循环操作 const item = document.querySelector('.item'); for (let i = 0; i < 10; i++) { item.style.left = i + 'px'; }这条规则和前面“批量读写避免重排”可以配套使用。总结起来就是:能缓存就缓存、能合并就合并、能批量就批量,不让浏览器做无用功。
5. 图片与多媒体优化:图片是首屏体积的“头号玩家”
图片往往是网页中体积占比最大的资源类型。雅虎军规在图片上的建议非常朴素:选对格式、压缩到极致、正确声明尺寸。今天可选的图片格式和方法比当年多得多,但核心原则依然是这三条。
5.1 选择正确的图片格式:JPEG、PNG、WebP怎么选
不同格式适合不同的图片内容,选错了格式,再强的压缩工具也救不回来。一张内容复杂的风景照用PNG保存,体积可能是JPEG的十几倍;一张包含大面积扁平颜色的Logo用JPEG保存,边缘会又糊又有色块。简单粗暴的选型规则如下:
- 照片、渐变、色彩复杂的图像:选JPEG或WebP(有损)。
- Logo、图标、文字截图、边缘清晰的图:选PNG或WebP(无损)。
- 动图表情包、简单交互动画:选WebP动图或GIF(WebP动图体积更小)。
- 矢量图形:优先用SVG,能缩放不模糊,体积可控。
WebP是目前综合表现最好的格式,结合有损和无损优势,比JPEG体积小20%~30%,同时支持透明。唯一需要注意的是兼容性,但在主流浏览器已经全面支持的情况下,基本可以放心使用。更激进的AVIF格式压缩率更高,但编码和解码性能目前还不够稳定,建议在核心图片场景小范围试点。
5.2 图片压缩与尺寸匹配:别拿“原图”糊在缩略图上
很多开发者在切图时直接扔原图给前端,前端又用width和height属性强行缩小显示。这样做的问题是:浏览器照样要下载原图的全部字节,白白浪费流量。正确做法是根据实际显示尺寸输出对应分辨率的图片,比如缩略图就输出100×100的小图,轮播图就输出不超过1920宽的图。移动端还要考虑DPR(设备像素比),iPhone等高分屏需要2x甚至3x的图,最佳实践是用srcset配合picture元素,让浏览器根据设备自动选择:
<picture> <source srcset="image-640.webp" type="image/webp" media="(max-width: 640px)"> <source srcset="image-1280.webp" type="image/webp"> <img src="image-1280.jpg" alt="示例图片" loading="lazy"> </picture>图片压缩工具方面,TinyPNG、Squoosh、ImageOptim都是实操中不错的选择。Squoosh支持WebP、AVIF等较新格式,能直观比较压缩前后体积和质量差异,我基本每次都要用它调参数。实际上,图片优化的收益往往比代码优化来得更快——只需把一堆2MB的图片压到200KB,整站速度立刻就能看到提升。
5.3 使用CSS Sprites或图标字体:把“一堆小图”合成“一张大图”
CSS Sprites(雪碧图)是雅虎军规中比较有年代感的条目,当年的小图标优化全靠它。原理是把多个小图标拼到一张大图上,用background-position定位显示需要的部分,从而减少请求数。在现代工程化下,这个方案基本被图标字体(Icon Font)和SVG Symbol替代了。图标字体的好处是字体文件一次加载、彩色矢量、可通过CSS控制大小颜色;SVG Symbol则更适合需要多色彩的场景,因为图标字体只能使用单色(通过currentColor继承)。
我的建议是:图标数量少(少于20个)直接用内联SVG;图标数量多、风格一致且只需单色,用SVG Symbol或图标字体;雪碧图只保留在一些极端场景,比如老项目维护中不方便引入新构建工具时。这个优化看着不起眼,但当你页面里有几十个小图标、每个都是一次HTTP请求时,合一的收益非常惊人。我见过一个营销页,把34个图标从“34张独立图片”改为“1张SVG Sprite”后,首屏请求数减少33个,这种优化带来的性能提升是物理级别的。
6. Cookie与数据交互优化:每一分带宽都要“花在刀刃上”
Cookie是浏览器存储在客户端的小数据块,但用得不好会成为性能隐形杀手。雅虎军规在Cookie上的建议是:控制数量、控制大小、隔离静态资源。这几条放在今天依然非常实用。
6.1 减少Cookie体积:请求头里的隐形重量
每次浏览器向同一个域名发起请求时,都会自动携带该域名的Cookie放在请求头里。一个Cookie几百字节看起来不起眼,但页面有100个请求时,总开销就会变成几十KB,在弱网下就是肉眼可见的延迟。更严重的是,有些项目喜欢往Cookie里塞用户信息、令牌、甚至是整个购物车数据,导致Cookie体积膨胀到好几KB甚至几十KB,每一个请求都在为这份臃肿买单。
优化方向有三个:第一,只存储必需数据,能放在服务端Session或本地存储(localStorage)的,就尽量不塞Cookie;第二,为Cookie设置合理的Path、Domain和过期时间,减少不必要携带的场景;第三,考虑将静态资源部署到独立域名,这样静态资源请求就不会携带业务Cookie,请求头体积直接瘦身。最后一点在2.2节CDN部分也提到过,它同时是CDN优化和Cookie优化的交叉区域,一举两得。
6.2 前端缓存与数据请求策略:不是所有数据都需要“实时拉取”
雅虎军规虽然没有明确系统讲述前端数据缓存策略,但“减少HTTP请求”的思想完全可以延伸到接口请求层面。API请求也有缓存策略:不变的字典数据(省市列表、枚举值、配置项)可以在首屏加载后缓存到内存或localStorage中,后续页面复用,不用重复请求。
具体到现代框架,SWR(Stale While Revalidate)、TanStack Query、Vue Query等库都内置了强大的缓存策略。它们的基本逻辑是:先用缓存数据立即渲染(提升感知速度),再在后台静默刷新数据(保证一致性)。这种“先展示旧数据、再悄悄更新”的模式,对用户体验的改善非常明显。我在一个C端项目中用了这个策略后,详情页的二次进入时间从“每次都等接口返回”变成“几乎秒开”。需要注意一点:有状态的数据(比如用户余额、订单状态)不要过度缓存,设置合理的staleTime,否则容易出现数据过期导致的业务问题。
7. 现代视角下的补充与修正:哪些规则被时代“升级”了
雅虎35条军规诞生于HTTP/1.1时代,今天的网络环境和浏览器能力都有了巨大变化。有些规则在新环境下需要修正,有些规则虽然形式上变了但本质依然成立。这一节把那些容易让人困惑的“新旧冲突”系统捋一遍。
7.1 HTTP/2下还需要合并资源吗:精细拆分的时代来了
HTTP/2引入了多路复用(Multiplexing),可以在一个TCP连接上并发发送多个请求,理论上请求数量的代价大幅下降了。因此有人说“合并文件已经过时,拆得越细越好”。这个说法有合理成分,但不完全对。多路复用解决了“请求数多导致排队”的问题,但没解决“每个请求头是重复开销”的问题,也依然受TCP拥塞窗口的影响。实际经验是:HTTP/2下不必像HTTP/1.1那样追求极限的请求合并,把资源拆成合理的粒度、配合服务端推送和预加载,效果往往更好。
比如,一个大型应用拆成几十个组件级模块,在HTTP/2下按需加载完全没问题,每个模块可以被高效并发下载。但如果拆得太细(几百个小文件),请求头开销和模块解析开销又可能抵消掉并发优势。我的实操建议是:以路由和功能模块为粒度拆分,保持每个文件在几十KB到几百KB之间,不做极端拆分也不做极端合并。这也是现在主流构建工具默认的产物策略。
7.2 页面缓存与PWA:让“二次打开”变成“瞬时响应”
雅虎军规里的缓存主要指HTTP缓存,但今天的缓存手段已经扩展到Service Worker层面。PWA(渐进式Web应用)通过Service Worker实现应用壳(App Shell)缓存和接口数据缓存,用户在弱网或断网状态下依然能访问页面内容,二次打开时的加载速度可以接近原生应用。
我建议在关键业务项目上引入Service Worker做静态资源预缓存。以Vite为例,通过vite-plugin-pwa插件,配置几行代码就能实现离线可用和自动更新:
import { VitePWA } from 'vite-plugin-pwa'; export default { plugins: [ VitePWA({ registerType: 'autoUpdate', manifest: { name: '某业务系统', short_name: '业务系统', start_url: '/', }, workbox: { globPatterns: ['**/*.{js,css,html,ico,png,svg}'], maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, }, }), ], };这个改动带来的体感提升很直接:第二次打开页面时静态资源直接从本地缓存读取,网络请求数归零,首屏几乎瞬时呈现。需要注意,Service Worker缓存策略要谨慎设计,尤其要避免“缓存了旧版本HTML导致用户永远看到老页面”的问题。autoUpdate模式是相对安全的实践。
7.3 浏览器渲染机制变化:DOMContentLoaded不再是唯一指标
雅虎军规时代衡量性能的主要指标是onload时间,今天业界已经有了更科学的指标集:LCP(Largest Contentful Paint)、FID(First Input Delay)、CLS(Cumulative Layout Shift),合起来被称为Core Web Vitals。这些指标更贴近用户真实体验:LCP衡量最大内容块的渲染速度,FID衡量交互响应速度,CLS衡量页面布局稳定性。
做性能优化时,建议优先关注这三个指标,而不是死盯onload。例如,onload等所有资源加载完才触发,但用户可能在所有资源加载完之前已经看到主要内容并开始交互了。LCP可能早已达标,onload却因为一个埋在底部的统计脚本变得很慢。反过来也一样。用对指标,才能定位对问题。推荐用Lighthouse(模拟场景)和真实用户监控(RUM,Real User Monitoring)工具配合,Lighthouse拿到实验室数据,RUM拿到真实场景的历史数据,两者结合才能看出优化到底有没有效果。
8. 实战排坑与工具推荐:我踩过的几个典型坑
讲完规则,分享几个我在实际项目中踩过的坑。这些坑各有代表性,有编译层面的、有缓存策略层面的、也有排查路径的问题。记录下来,希望你能少走点弯路。
8.1 坑一:CDN缓存设置不当导致“发布后用户还在用旧版本”
曾经在一次版本发布后,有用户反馈“页面还是旧的”。排查后发现静态资源文件名没有带内容哈希,CDN上缓存了旧文件,导致发布后相当一部分用户命中了旧缓存。从那以后我强制要求所有静态资源产物文件名必须带md5哈希,并且在运维层面约定“静态资源长缓存、HTML不做缓存”的规范。
这里的关键是理解缓存策略的联动关系:HTML不缓存,静态资源长缓存。每次用户访问页面,HTML都会从服务器重新获取,拿到最新的文件名版本,再去请求带新哈希的静态资源。如果HTML被缓存了,整个版本更新链路就断了。另外,发布时要注意CDN刷新策略。很多云厂商支持“自动回源并刷新缓存”的配置,或者提供“目录刷新”功能,发布脚本里应该自动触发。
8.2 坑二:追求极致压缩导致代码被Tree Shaking“误伤”
某个组件引入了一个库,只用了其中一两个函数,当时打包后文件体积确实小了。结果上线后控制台报错,报错信息指向“某个函数未定义”。排查发现这个库的某些API依赖“副作用”代码,默认的Tree Shaking策略把带副作用的模块当作“无用模块”摇掉了。
解决方案是配置sideEffects字段,显式告诉打包工具哪些模块不允许被摇掉。Webpack和Rollup都有对应配置。具体做法:在package.json里加上"sideEffects": ["*.css", "*.scss", "./src/global/*"],把实际有副作用的文件保下来。从那以后我再也不盲目追求“shake得越干净越好”,而是先跑通构建,再逐步检查哪些“摇掉”是安全的。性能优化和业务正确性之间,业务正确性永远是第一位。
8.3 坑三:混淆了“开发环境”和“生产环境”的性能测试结果
另一个高频问题是在本地开发环境做性能测试,然后得出“页面很慢”或“优化无效”的结论。开发环境有未压缩的源码、热更新逻辑、source map,生产环境则有压缩、合并、CDN缓存等。两者的数据没有任何可比性。
我的建议是:性能测试和优化验证一律基于生产构建产物,最好在部署到预发布环境后做,有条件的话用独立的线上URL测试。本地环境只适合定位代码逻辑问题,不适合评估性能数据。另外,性能优化做前后对比时,要保证变量控制——同一网络环境、同一浏览器配置、同一测试页面,最好多跑几次取中位数,避免偶发波动误导判断。工具方面,Chrome DevTools的Performance和Network面板、Lighthouse、WebPageTest是我日常用得最多的三件套,配合前面提到的RUM工具,基本能覆盖“实验室+真实场景”的完整评估链路。
9. 一份可以直接拿去的检查清单
把全文的核心要点沉淀为一份检查清单,方便你在项目上线前逐项核验。这并不是说要死板地照抄全部,而是把它当作报错排查的地图,至少能帮你找到大多数性能问题的方向。
- 网络请求数:首屏关键资源请求数是否已经压到合理范围?
- CDN加速:静态资源是否部署在CDN并设置合理回源策略?
- 压缩:文本类资源是否开启gzip或Brotli压缩?
- 缓存头:带哈希的静态资源是否设置长缓存?
- HTML缓存:HTML页面是否设置了合理的
no-cache策略? - CSS位置:风格样式是否都在
<head>中同步加载? - JS位置:功能性脚本是否使用了
defer或放在了页面尾部? - 图片格式:图片是否选择了正确格式并经过压缩?
- 图片尺寸:每个
<img>是否输出了匹配显示尺寸的资源? - DOM数量:页面是否存在过大的DOM树?长列表是否用了虚拟滚动?
- 重复脚本:是否存在重复依赖或重复加载的公共代码?
- Cookie体积:业务Cookie是否控制在合理小体积?
- 静态资源域名:静态资源是否与业务接口分离部署?
- 现代指标:LCP、FID、CLS三项核心指标是否达标?
- 构建产物:代码是否经过了压缩、Tree Shaking和合理分包?
我个人在实际操作中的体会是:性能优化不是一次性的工作,而是每次迭代都要关注的持续性工程。雅虎35条军规的价值不在于每一条都“神”,而在于它训练了一种思维习惯——拿到一个页面,第一反应不是“差不多能用就行”,而是会去想“这一步请求能不能省、这一步渲染能不能减”。这套清单至今依然是我做页面性能评估的起点,也建议你从这张清单开始,逐步建立起自己的排查路径。