做前端性能优化是绕不开的话,题也是最容易踩坑的地方。

最近我在做一个项目的性能优化踩了不少坑熬了好几个通宵才把问题一个个解决。有些问题看起来简单,但是排查起来非常费劲有些问题更是隐藏得很深不仔细分析根本发现不了。

今天想记录一下我在前端性能优化过程中遇到的那些让我熬夜的问题包括排查过程和,解决方案希望能帮大家少走弯路。

一、图片优化:看似简单坑最多

第一个让我熬夜的问题是图片优化。

项目上线后发现首屏加载很慢打开Chrome DevTools一看Network面板里图片占了大部分的加载时间特别是首页的轮播图一张就几MB加载很慢。

问题1:图片格式不对

我发现项目里很多图片用的是PNG格式,但是这些图片都是照片类的用JPG或者WebP更合适体积能小很多。

比如一张轮播图PNG格式3.2MB转成JPG质量80只有400KB转成WebP质量80只有250KB差距非常大。

解决方案:把所有照片类的图片从PNG转成JPG或者WebPWebP兼容性在2018年还不是特别好,所以用picture标签做降级支持WebP的浏览器用WebP不支持的用JPG。

问题2:图片尺寸太大

还有一个问题图片的原始尺寸太大,比如一张产品缩略图显示,只有200x200但是原始图片是2000x2000加载了很大的图片,但是显示很小浪费带宽。

解决方案:根据显示尺寸生成不同尺寸的图片用srcset属性让浏览器根据屏幕分辨率和,显示尺寸选择合适的图片。比如生成200x200400x400800x800三种尺寸浏览器自动选择。

问题3:没有懒加载

项目里很多图片在首屏以下,但是页面加载的时候,就全部加载了导致首屏加载慢浪费带宽。

解决方案:用Intersection Observer API实现图片懒加载首屏的图片正常加载首屏以下的图片滚动到可视区域再加载。2018年Intersection Observer兼容性还可以不支持的浏览器用scroll事件降级。

问题4:没有用CDN

图片都放在自己的服务器上没有用CDN加载速度慢特别是不同地区的用户访问延迟高。

解决方案:把图片放到CDN上用CDN加速加载速度提升很多特别是图片多的网站效果明显。

这几个图片的问题看起来简单,但是一个个排查和修改花了我一个通宵特别是批量处理图片生成不同尺寸和格式写了脚本跑了很久。

二、打包体积:一个依赖拖垮整个项目

第二个让我熬夜的问题是打包体积。

项目用Webpack打包发现打包后的JS文件很大首屏JS就有1.5MBgzip后,还有500KB加载很慢。

问题1:引入了整个库

我用webpack-bundle-analyzer分析打包结果发现一个UI组件库占了很大的体积,因为我在代码里import了整个库而不是按需引入需要的组件。

比如我只用了Button和Input两个组件,但是import了整个组件库导致所有组件都被打包进去了体积大了很多。

解决方案:改成按需引入只import用到的组件用babel-plugin-import或者,手动import具体的组件文件体积减少了很多。

问题2:没有代码分割

所有JS都打包到一个文件里首屏加载了所有页面的代码,但是用户可能只看首页其他页面的代码根本用不到浪费带宽。

解决方案:用Webpack的代码分割(Code Splitting)按路由分割每个页面的代码单独打包首屏只加载首页的代码其他页面的代码按需加载。用动态import(import())实现路由级别的代码分割。

问题3:第三方库没有单独打包

第三方库(vendor)和业务代码打包到一起每次修改业务代码第三方库也会重新打包hash变化用户需要重新下载包括第三方库在内的整个JS文件浪费带宽。

解决方案:用Webpack的CommonsChunkPlugin(Webpack 4用splitChunks)把第三方库单独打包业务代码修改不影响第三方库的hash用户只需要下载变化的业务代码第三方库用缓存不用重新下载。

问题4:没有Tree Shaking

一些工具库我只用了其中几个函数,但是整个库都被打包进去了,因为没有开启Tree Shaking。

解决方案:确保所有代码用ES6模块(import/export)不要用CommonJS(require/module.exports)Webpack的Tree Shaking只对ES6模块有效。在package.json里设置"sideEffects": false告诉Webpack哪些文件没有,副作用可以安全地Tree Shaking。

这几个打包的问题排查和修改花了我两个通宵特别是代码分割和Tree Shaking改了很多代码测试了很久才确保功能正常体积也降下来了。

最后首屏JS从1.5MB降到了300KBgzip后,只有100KB提升非常明显。

三、首屏加载:白屏时间太长

第三个让我熬夜的问题是首屏加载白屏时间太长。

用户打开网站要等好几秒才能看到内容白屏时间太长用户体验很差很多用户等不及就关掉了。

问题1:JS阻塞渲染

JS文件放在head里没有加async或者defer浏览器加载JS的时候,会阻塞页面渲染导致白屏时间长。

解决方案:把JS文件放到body末尾,或者加defer属性让JS异步加载不阻塞页面渲染。注意async和defer的区别async加载完就执行可能打乱执行顺序defer按顺序在DOM解析完后执行更安全。

问题2:CSS太大阻塞渲染

CSS文件太大加载慢浏览器必须等CSS加载完才能渲染页面导致白屏时间长。

解决方案

  1. 压缩CSS去掉无用的CSS用PurifyCSS或者,UnCSS去掉没有用到的CSS。
  2. 把首屏需要的CSS内联到HTML里(Critical CSS)首屏不用等CSS文件加载就能渲染剩下的CSS异步加载。
  3. 用媒体查询分割CSS不同媒体的CSS单独文件按需加载。

问题3:没有服务端渲染

项目是纯客户端渲染(CSR)浏览器必须等JS加载执行完才能渲染内容白屏时间长SEO也不好。

解决方案:考虑加服务端渲染(SSR)或者预渲染(Prerendering)首屏内容在,服务端渲染好直接返回HTML浏览器不用等JS就能看到内容白屏时间大大缩短。

但是SSR改造成本大我们项目时间紧最后用了预渲染方案用prerender-spa-plugin在,构建时把关键页面预渲染成静态HTML成本小效果也不错。

问题4:没有用HTTP/2

服务器用的还是HTTP/1.1多个资源请求需要排队加载慢特别是图片多的页面更明显。

解决方案:升级到HTTP/2HTTP/2支持多路复用多个请求可以并行加载不用排队加载速度提升很多。需要服务器支持HTTPS和HTTP/2Nginx配置一下就可以。

这几个首屏的问题排查和修改花了我一个通宵特别是Critical CSS和,预渲染调试了很久才确保首屏内容正确显示。

最后首屏白屏时间从3秒降到了0.5秒提升非常明显。

四、渲染性能:页面卡顿

第四个让我熬夜的问题是渲染性能页面卡顿。

特别是列表页数据多的时候,滚动卡顿点击反应慢用户体验很差。

问题1:列表没有虚拟滚动

列表页一次渲染了几百条数据DOM节点太多渲染慢内存占用高滚动卡顿。

解决方案:用虚拟滚动(Virtual Scroll)只渲染可视区域的DOM节点滚动时动态更新DOM大大减少DOM节点数量提升渲染性能。用vue-virtual-scroller或者react-virtualized等库实现。

问题2:频繁的重排重绘

代码里有很多频繁修改DOM样式的操作导致频繁的重排(Reflow)和,重绘(Repaint)页面卡顿。

比如在循环里逐个修改元素的样式每次修改都会触发重排性能差。

解决方案

  1. 批量修改DOM样式先把元素display:none或者用DocumentFragment修改完再显示只触发一次重排。
  2. 用transform和opacity做动画这两个属性不会触发重排只会触发合成(Composite)性能好。
  3. 用will-change提示浏览器哪些元素会变化让浏览器提前优化,但是不要滥用,否则反而占用更多内存。
  4. 用requestAnimationFrame把DOM修改放到下一帧和,浏览器刷新同步避免卡顿。

问题3:事件绑定太多

列表里每个元素都绑定了事件几百个元素就有几百个事件监听器内存占用高性能差。

解决方案:用事件委托(Event Delegation)把事件绑定到父元素上利用事件冒泡处理子元素的事件只需要一个事件监听器大大减少内存占用。

问题4:没有防抖节流

scrollresizeinput等事件触发频繁没有做防抖(Debounce)和,节流(Throttle)导致事件处理函数执行太频繁页面卡顿。

解决方案

  1. 防抖:事件触发后等一段时间再执行,如果这段时间内又触发了重新计时适合search输入等场景。
  2. 节流:固定时间内只执行一次适合scrollresize等场景。

用lodash的debounce和throttle函数,或者自己实现都可以。

这几个渲染的问题排查和修改花了我两个通宵特别是虚拟滚动和,重排优化改了很多代码测试了很久才确保功能正常页面流畅。

最后列表页滚动从卡顿变成流畅60fps提升非常明显。

五、内存泄漏:页面越用越卡

第五个让我熬夜的问题是内存泄漏。

页面刚打开的时候,很流畅,但是用了一段时间后越来越卡最后甚至浏览器崩溃刷新一下又好了这是典型的内存泄漏。

问题1:事件监听器没有移除

组件销毁的时候,没有移除绑定的事件监听器导致内存泄漏组件反复创建销毁事件监听器越来越多内存占用越来越高。

比如在mounted里绑定了window的scroll事件,但是在beforeDestroy里没有移除组件销毁后事件监听器还在内存泄漏。

解决方案:组件销毁时移除所有绑定的事件监听器在beforeDestroy(Vue)或者,componentWillUnmount(React)里移除事件监听器定时器等等。

问题2:定时器没有清除

代码里有setInterval或者setTimeout但是组件销毁时没有清除定时器还在运行内存泄漏。

解决方案:组件销毁时清除所有定时器用clearInterval和,clearTimeout保存定时器的ID销毁时清除。

问题3:闭包引用了大对象

闭包引用了大的对象,或者DOM元素导致这些对象无法被垃圾回收内存泄漏。

比如事件回调函数里引用了大的数据对象,或者DOM元素事件监听器没有移除这些对象就一直被引用无法回收。

解决方案

  1. 避免闭包引用大的对象需要的数据尽量用局部变量不要引用外部的大对象。
  2. 组件销毁时把引用的大对象置为null帮助垃圾回收。
  3. 及时移除事件监听器和定时器避免闭包一直被引用。

问题4:没有清理的全局变量

代码里不小心创建了全局变量(没有用var/let/const声明)这些变量一直存在不会被回收内存泄漏。

解决方案:开启严格模式('use strict')未声明的变量会报错避免意外创建全局变量。代码审查时注意有没有未声明的变量。

这几个内存泄漏的问题排查非常费劲用Chrome DevTools的Memory面板拍堆快照对比分析找了很久才找到泄漏点花了我一个通宵。

最后内存泄漏问题解决页面长时间使用也不会越来越卡了。

六、其他踩坑

除了上面的几个大问题,还有一些小坑也让我熬了夜。

问题1:字体加载慢

用了自定义字体字体文件大加载慢导致页面文字一开始不显示,或者闪烁(FOIT/FOUT)。

解决方案

  1. 用font-display: swap字体加载完之前,用降级字体显示加载完再切换避免文字不显示。
  2. 字体文件压缩用woff2格式体积小加载快。
  3. 用preload预加载关键字体让浏览器提前加载。

问题2:第三方脚本阻塞

引入了第三方统计脚本广告脚本等等这些脚本加载慢,或者出错会阻塞页面渲染影响性能。

解决方案

  1. 第三方脚本异步加载加async或者defer不阻塞页面。
  2. 延迟加载非关键的第三方脚本页面加载完再加载。
  3. 给第三方脚本加错误处理避免第三方脚本出错影响自己的代码。

问题3:没有利用缓存

静态资源没有设置缓存策略每次访问都重新下载浪费带宽加载慢。

解决方案

  1. 静态资源(JS/CSS/图片)设置长缓存(Cache-Control: max-age=31536000)文件名加hash内容变化hash变化用户自动加载新文件。
  2. HTML设置短缓存,或者不缓存确保用户能拿到最新的HTML。
  3. 用Service Worker做离线缓存提升二次访问速度和离线体验。

七、性能优化的心得

经过这次性能优化踩了这么多坑熬了这么多夜我有一些心得。

心得1:性能优化要趁早

性能优化不要等项目上线后才做要在开发阶段就考虑性能,比如图片压缩按需引入代码分割等等在开发时就做好比后期再优化成本小很多。

心得2:用数据说话

性能优化不要凭感觉要用数据说话用Chrome DevToolsLighthouseWebPageTest等工具测量性能数据找到瓶颈针对性优化优化后再测量对比看效果。

心得3:不要过度优化

性能优化要适度不要过度优化,比如为了减少几KB的体积把代码写得很难维护得不偿失。性能优化要在性能和可维护性之间,找平衡优先优化影响大的瓶颈不要纠结小的优化点。

心得4:持续监控

性能优化不是一次性的工作要持续监控项目迭代后可能会引入新的性能问题要定期检查性能数据及时发现和解决问题。

八、写在最后

以上就是我在前端性能优化过程中遇到的那些让我熬夜的问题和解决方案。

性能优化是一个持续的过程也是一个不断踩坑和学习的过程每个项目都会遇到不同的问题需要具体问题具体分析。

希望我的踩坑经历能帮大家少走弯路在性能优化的路上少熬点夜。

最后用一句话结束这篇文章:"性能优化没有最好,只有更好持续优化持续进步。"

愿大家的网站都能又快又稳用户体验棒棒的。