最近接手了一个老项目的前端性能优化任务,这个项目首屏加载时间长达8秒,用户体验很差,老板要求必须优化。
经过一个月的优化,首屏加载时间从8秒降到了1.5秒,提升了5倍多,用户体验大幅改善,老板也很满意。
今天就来分享这次前端性能优化的完整过程,包括性能分析、资源优化、代码优化、网络优化、运行时优化等各个方面,以及优化过程中踩过的坑和总结的经验。
一、性能分析:先测量,再优化
做性能优化,第一步不是急着改代码,而是先测量,找到性能瓶颈在哪里。
工具一:Chrome DevTools
Chrome DevTools是最常用的前端性能分析工具。Network面板可以看每个资源的加载时间、大小、请求顺序;Performance面板可以看页面运行时的性能,包括JS执行、布局、绘制等;Audits面板(现在叫Lighthouse)可以给出综合的性能评分和优化建议。
我首先用Lighthouse跑了一下,得分只有35分(满分100),属于很差的水平。Lighthouse给出了很多优化建议,比如减少未使用的JavaScript、正确调整图片大小、延迟加载首屏外的图片、启用文本压缩等。
然后用Network面板看了一下资源加载情况,发现:
- 首屏需要加载200多个请求,总大小超过5MB
- JavaScript文件有3MB多,其中很多是用不到的代码
- 图片没有压缩,很多图片都是几MB的大图
- 没有启用Gzip压缩
- 没有设置缓存策略
工具二:WebPageTest
WebPageTest是一个在线的网站性能测试工具,可以模拟不同地区、不同网络环境、不同浏览器的加载情况,还能生成Waterfall图,看每个请求的详细时间线。
我用WebPageTest测了一下,发现首字节时间(TTFB)就有2秒多,说明服务器响应也慢。而且关键渲染路径很长,CSS和JS阻塞了页面渲染。
工具三:webpack-bundle-analyzer
因为项目是用Webpack打包的,我用webpack-bundle-analyzer分析了打包产物,发现:
- 打包后的vendor.js有2MB多,包含了很多第三方库
- 很多第三方库全量引入了,比如lodash、moment.js,其实只用到了很小一部分
- 业务代码没有按路由分割,所有页面的代码都打包在一起了
通过这些分析工具,我找到了性能瓶颈主要在四个方面:资源体积太大、请求太多、服务器响应慢、运行时性能差。
二、资源优化:减小体积,减少请求
找到瓶颈之后,开始优化。首先是资源优化,减小资源体积,减少请求数量。
优化一:代码分割,按需加载
这是效果最明显的优化。原来的项目把所有代码都打包在一个文件里,不管用户访问哪个页面,都要加载全部代码。
我用Webpack的code splitting功能,把代码按路由分割,每个页面的代码单独打包,用户访问哪个页面才加载哪个页面的代码。同时,把第三方库也单独打包,利用浏览器缓存。
配置很简单,在Webpack的output里加一个chunkFilename,然后在路由配置里用动态import()就可以了:
// 路由配置
const Home = () => import('./pages/Home.vue');
const About = () => import('./pages/About.vue');
const User = () => import('./pages/User.vue');这样改完之后,首屏只需要加载首页的代码,JS体积从3MB降到了500KB,效果非常明显。
优化二:Tree Shaking,移除未使用的代码
项目里很多第三方库是全量引入的,比如lodash,其实只用到了几个函数,但是把整个lodash都打包进去了。
我用Tree Shaking来移除未使用的代码。首先确保Webpack的mode设置为production,这样会自动开启Tree Shaking。然后,把全量引入改成按需引入:
// 改之前:全量引入
import _ from 'lodash';
// 改之后:按需引入
import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';对于moment.js,我用了一个更轻量的替代方案day.js,API和moment.js差不多,但是体积只有2KB,比moment.js的几十KB小多了。
优化三:图片优化
图片是资源体积的大头。项目里很多图片都是几MB的大图,没有压缩,也没有用合适的格式。
我做了以下优化:
- 压缩图片:用TinyPNG或者imagemin批量压缩图片,很多图片压缩后体积减少了70%以上,画质几乎没有损失。
- 用WebP格式:WebP格式比JPEG和PNG小25%-35%,而且支持透明和动画。我用Webpack的image-webpack-loader自动把图片转成WebP格式,同时保留JPEG/PNG作为降级方案。
- 懒加载:首屏外的图片用懒加载,滚动到可视区域才加载。用Intersection Observer API实现,或者用vue-lazyload等库。
- 响应式图片:用srcset和sizes属性,根据屏幕大小加载不同尺寸的图片,手机上不用加载PC端的大图。
- 雪碧图:小图标合并成雪碧图,减少请求数量。不过现在更推荐用SVG图标或者iconfont,更灵活。
优化四:启用Gzip/Brotli压缩
原来的服务器没有启用Gzip压缩,文本类资源(HTML、CSS、JS)都是明文传输,体积很大。
我在Nginx上启用了Gzip压缩,压缩级别设为6,压缩类型包括text/plain、text/css、application/javascript、application/json等。启用之后,文本类资源体积减少了60%-70%。
如果服务器支持,还可以用Brotli压缩,比Gzip压缩率更高,但是兼容性稍差,可以和Gzip一起启用,浏览器支持哪个就用哪个。
优化五:字体优化
项目里用了自定义字体,字体文件有好几百KB,而且阻塞了页面渲染。
我做了以下优化:
- 用font-display: swap,让字体加载期间先用系统字体,不阻塞渲染。
- 只加载需要的字符,用font-spider等工具提取用到的字符,减小字体文件体积。
- 用WOFF2格式,比TTF和OTF小很多。
- 预加载字体文件,用<link rel="preload">提前加载关键字体。
三、网络优化:减少延迟,加快传输
资源体积减小了,接下来优化网络传输,减少延迟,加快传输速度。
优化一:CDN加速
原来的静态资源都放在自己的服务器上,用户访问的时候延迟比较高,特别是跨地区访问。
我把静态资源(JS、CSS、图片、字体)都放到了CDN上,CDN会把资源缓存到离用户最近的节点,用户访问的时候直接从最近的节点获取,延迟大大降低。
同时,CDN还能减轻源站的压力,提高网站的可用性和稳定性。
优化二:HTTP缓存策略
原来的项目没有设置合理的缓存策略,每次访问都要重新下载所有资源,浪费带宽,也影响速度。
我设置了合理的缓存策略:
- HTML文件:no-cache,每次都验证,确保用户看到最新版本。
- 静态资源(JS、CSS、图片、字体):Cache-Control: public, max-age=31536000, immutable,缓存一年,因为文件名里有hash,内容变了文件名也会变,不用担心缓存过期的问题。
这样设置之后,用户第二次访问的时候,静态资源直接从浏览器缓存读取,不需要再请求服务器,速度非常快。
优化三:DNS预解析和预连接
对于第三方域名的资源(比如CDN、统计代码、字体等),可以用DNS预解析和预连接,提前建立连接,减少延迟。
<!-- DNS预解析 -->
<link rel="dns-prefetch" href="//cdn.example.com">
<!-- 预连接 -->
<link rel="preconnect" href="//cdn.example.com" crossorigin>这样,浏览器在解析HTML的时候,就会提前解析DNS、建立连接,等真正需要加载资源的时候,连接已经建立好了,节省了时间。
优化四:服务端渲染(SSR)
这个项目是纯客户端渲染的SPA,首屏需要等JS加载、解析、执行完才能渲染内容,白屏时间比较长。
对于首屏性能要求高的页面,我考虑用服务端渲染(SSR),在服务器端把页面渲染成HTML,直接返回给浏览器,浏览器拿到HTML就能显示内容,不需要等JS加载完。
不过SSR改造成本比较大,我先做了其他优化,效果已经很好了,SSR留到以后再做。如果你的项目首屏性能要求很高,而且有SEO需求,可以考虑SSR。
四、运行时优化:让页面更流畅
加载速度优化了,接下来优化运行时性能,让页面交互更流畅,不卡顿。
优化一:减少重排重绘
频繁的DOM操作会导致重排(reflow)和重绘(repaint),影响页面性能。
我做了以下优化:
- 批量操作DOM,不要频繁操作。比如要添加多个元素,先创建一个DocumentFragment,把元素都加进去,最后一次性插入DOM。
- 用class批量修改样式,不要逐个修改style属性。
- 对需要频繁操作的元素,用绝对定位或者fixed定位,脱离文档流,减少重排的影响范围。
- 用transform和opacity做动画,这两个属性不会触发重排,只会触发合成层,性能很好。
优化二:防抖和节流
对于频繁触发的事件(比如scroll、resize、input、mousemove),要用防抖(debounce)和节流(throttle)来减少事件处理函数的执行次数,避免卡顿。
// 防抖:事件触发后等待n秒再执行,如果n秒内又触发了,重新计时
function debounce(fn, delay) {
let timer = null;
return function() {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, arguments), delay);
};
}
// 节流:n秒内只执行一次
function throttle(fn, delay) {
let last = 0;
return function() {
const now = Date.now();
if (now - last >= delay) {
last = now;
fn.apply(this, arguments);
}
};
}比如搜索框的input事件用防抖,滚动事件用节流,能大幅减少JS执行次数,页面更流畅。
优化三:虚拟列表
项目里有一个长列表页面,一次渲染几千条数据,DOM节点太多,页面卡顿,滚动不流畅。
我用虚拟列表(Virtual List)来优化,只渲染可视区域内的DOM节点,滚动的时候动态更新内容。不管数据有多少条,DOM节点始终只有几十个,性能非常好。
可以用vue-virtual-scroller或者react-virtualized等库来实现,也可以自己写一个简单的虚拟列表组件。
优化四:Web Worker
项目里有一些复杂的计算(比如数据处理、图表计算),放在主线程执行会阻塞UI,导致页面卡顿。
我把这些复杂计算放到Web Worker里执行,Web Worker在后台线程运行,不会阻塞主线程,计算完之后把结果发回主线程。这样主线程只负责UI渲染,页面更流畅。
不过Web Worker不能操作DOM,也不能访问window对象,只能做纯计算,使用的时候要注意。
优化五:requestAnimationFrame
对于动画和高频操作,用requestAnimationFrame代替setTimeout或者setInterval,requestAnimationFrame会在浏览器重绘之前执行,和浏览器的刷新频率同步,动画更流畅,也更节省性能。
function animate() {
// 更新动画状态
element.style.transform = `translateX(${x}px)`;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);五、服务器优化:加快响应速度
前面说过,这个项目的首字节时间(TTFB)有2秒多,服务器响应慢也是一个问题。
优化一:服务器缓存
对于不经常变化的页面或者数据,在服务器端加缓存,比如用Redis缓存页面内容或者数据库查询结果,用户请求的时候直接从缓存返回,不需要查询数据库,响应速度大大提升。
优化二:数据库优化
慢查询是服务器响应慢的常见原因。我用数据库的慢查询日志找到了几个慢查询,然后给相关字段加了索引,查询速度从几秒降到了几毫秒。
同时,避免SELECT *,只查询需要的字段;避免N+1查询,用JOIN或者批量查询;对于复杂查询,考虑用缓存或者异步处理。
优化三:开启Keep-Alive
在服务器上开启HTTP Keep-Alive,让多个请求复用同一个TCP连接,减少TCP握手的开销,加快请求速度。
Nginx默认就开启了Keep-Alive,保持时间默认是75秒,可以根据需要调整。
六、优化效果
经过一个月的优化,效果非常明显:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 首屏加载时间 | 8秒 | 1.5秒 | 5.3倍 |
| Lighthouse得分 | 35分 | 92分 | 提升57分 |
| 首屏请求数 | 200+ | 30+ | 减少85% |
| 首屏资源大小 | 5MB+ | 800KB | 减少84% |
| JS体积 | 3MB | 500KB | 减少83% |
| TTFB | 2秒 | 300ms | 6.7倍 |
用户体验大幅改善,页面加载快了,交互也流畅了,用户投诉减少了很多,老板也很满意。
七、踩过的坑
优化过程中也踩了不少坑,这里分享一下。
坑一:代码分割后缓存失效
代码分割之后,每个chunk的文件名里有hash,内容变了hash也会变,这本来是好事。但是如果公共chunk(比如vendor)的内容变了,所有依赖它的chunk的hash都会变,导致缓存大面积失效。
解决方案:用Webpack的runtimeChunk,把webpack的运行时代码单独抽出来;用splitChunks合理配置公共代码的拆分策略,尽量让公共chunk稳定,不经常变。
坑二:懒加载导致首屏闪烁
图片懒加载之后,图片加载出来的时候会导致页面布局跳动(CLS),用户体验不好。
解决方案:给图片设置固定的宽高比,用padding-top的方式占位,图片加载出来之前就占据空间,不会导致布局跳动。
坑三:Gzip压缩级别太高影响CPU
一开始我把Gzip压缩级别设为9(最高),虽然压缩率高了一点,但是服务器CPU占用很高,响应速度反而变慢了。
解决方案:压缩级别设为6就够了,压缩率和9差不多,但是CPU占用低很多。或者用预压缩,构建的时候就生成.gz文件,服务器直接返回,不需要实时压缩。
坑四:CDN缓存更新不及时
静态资源放到CDN之后,有时候更新了代码,CDN缓存还没刷新,用户看到的还是旧版本。
解决方案:文件名里加hash,内容变了文件名也变,不存在缓存过期的问题。对于HTML文件,设置no-cache,每次都验证。如果需要强制刷新CDN缓存,可以用CDN提供的刷新接口。
八、总结和建议
这次前端性能优化,让我深刻体会到性能优化的重要性,也积累了很多经验。
建议一:性能优化要持续进行
性能优化不是一次性的事情,而是一个持续的过程。代码在不断迭代,新的功能、新的依赖都可能引入新的性能问题。要把性能监控纳入日常开发流程,比如用Lighthouse CI在每次提交的时候自动跑性能测试,发现性能下降及时处理。
建议二:关注用户体验,而不只是数字
性能优化的目标是提升用户体验,而不只是追求数字好看。比如首屏加载时间很重要,但是交互流畅度、视觉稳定性(CLS)也很重要。要从用户的角度出发,关注真实的用户体验,而不只是实验室里的数字。
建议三:预防胜于治疗
最好的性能优化是在开发阶段就注意性能,而不是等性能差了再去优化。比如开发的时候就注意代码分割、按需加载、图片压缩、避免不必要的DOM操作等,把性能意识融入日常开发中,这样后期优化的成本就会小很多。
建议四:根据项目实际情况选择优化方案
不同的项目,性能瓶颈不一样,优化方案也不一样。不要盲目套用别人的优化方案,要先测量,找到自己项目的性能瓶颈,然后针对性地优化。比如内容型网站,首屏加载和图片优化更重要;交互型应用,运行时性能更重要;电商网站,商品列表和图片优化更重要。
结语
前端性能优化是一个永无止境的话题,技术在不断发展,新的优化手段也在不断出现。但是核心思想是不变的:减小体积、减少请求、减少延迟、减少阻塞,让用户更快地看到内容,更流畅地交互。
希望这篇文章能给正在做前端性能优化的你一些参考和启发。如果你也有性能优化的经验或者问题,欢迎在评论区交流。
最后,愿每一个网站都能快如闪电,给用户最好的体验。
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录