什么是前端性能优化?
通常来说我们说的前端性能优化是指:从用户开始访问我们的网站到整个页面完整地展现出来的过程中,通过各种优化策略和优化方法,让页面加载的更快,让用户的操作响应更及时,给用户更好的使用体验。
前端性能优化是我们每个前端打工人都必须面对的问题,那怎么进行性能优化呢?下面我就给大家分享我所了解的一些性能优化知识,有什么疑问请大家提出来,谢谢~
性能优化指标与测量工具
工欲善其事必先利其器,在进行性能优化之前,我们应该准备好自己的性能优化测量工具,并且要有优化目标,即某项或者多项指标需要优化到什么程度。
常用的性能优化指标
- Speed Index(lighthouse,速度指数)
- TTFB(Network,第一个请求响应时间)
- 页面加载时间
- 首次渲染
- 交互动作的反馈时间
- 帧率FPS(动画 ctrl+shift+p)
- 异步请求完成时间
性能测量工具
-
Chrome DevTools
- 开发调试、性能评测
- Audit(Lighthouse)
- Throttling 调整网络吞吐
- Performance 性能分析
- Network 网络加载分析
-
Lighthouse
- 网站整体质量评估
- 还可以提出优化建议
-
WebPageTest
- 测试多地点(球各地的用户访问你的网站的性能情况)
- 全面性能报告(first view,repeat view,waterfall chart 等等)
- WebPageTest 还可以进行本地安装,让你的应用在还没上线的时候就可以测试。
- 更多请看 官网
常用的性能测量API
计算一些关键的性能指标
window.addEventListener('load', (event) => {
// Time to Interactive
let timing = performance.getEntriesByType('navigation')[0];
// let timing = performance.timing
console.log(timing.domInteractive);
console.log(timing.fetchStart);
let diff = timing.domInteractive - timing.fetchStart;
console.log("TTI: " + diff);
})
复制代码
以上代码只测量了”首次可交互时间”,其它常用的性能指标还有:
DNS 解析耗时: domainLookupEnd - domainLookupStart
TCP 连接耗时: connectEnd - connectStart
SSL 安全连接耗时: connectEnd - secureConnectionStart
网络请求耗时 (TTFB): responseStart - requestStart
数据传输耗时: responseEnd - responseStart
DOM 解析耗时: domInteractive - responseEnd
资源加载耗时: loadEventStart - domContentLoadedEventEnd
First Byte时间: responseStart - domainLookupStart
白屏时间: responseEnd - fetchStart
首次可交互时间: domInteractive - fetchStart
DOM Ready 时间: domContentLoadEventEnd - fetchStart
页面完全加载时间: loadEventStart - fetchStart
http 头部大小: transferSize - encodedBodySize
重定向次数:performance.navigation.redirectCount
重定向耗时: redirectEnd - redirectStart
复制代码
观察长任务(long tasks)
// 通过 PerformanceObserver 得到所有的 long task 对象
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry)
}
})
// 监听 long task
observer.observe({entryTypes: ['longtask']})
复制代码
监听页面可见性的状态
let vEvent = 'visibilitychange';
if (document.webkitHidden != undefined) {
// webkit prefix detected
vEvent = 'webkitvisibilitychange';
}
function visibilityChanged() {
if (document.hidden || document.webkitHidden) {
console.log("Web page is hidden.")
} else {
console.log("Web page is visible.")
}
}
document.addEventListener(vEvent, visibilityChanged, false);
复制代码
通过监听页面可见性的状态可以判断用户是否在看当前页面,可以做一些处理,比如视频暂停,保存状态等等
判断用户网络状态
var connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
var type = connection.effectiveType;
function updateConnectionStatus() {
console.log("Connection type changed from " + type + " to " + connection.effectiveType);
type = connection.effectiveType;
}
connection.addEventListener('change', updateConnectionStatus);
复制代码
通过判断用户网络状态,可以做一些针对性的加载,比如用户网络状态好,就可以加载一些更加高清的图片,反之加载一些质量更差的图片等等。
传输加载优化
- 减少 HTTP 请求
- 启用 Gzip 压缩
- 启用 Keep Alive 长连接(默认)
- HTTP 资源缓存
- Service workers
- 加速重复访问
- 离线支持
- 需要注意延长了首屏加载时间,但是页面总加载时间减少
- 启用 HTTP2
- 考虑用SSR技术
- 异步无阻塞加载JS(defer,async)
资源优化
- 资源压缩与合并
- 图片格式优化,选择合适的图片格式
- 图片加载优化(懒加载,渐进式加载,响应式图片,雪碧图)
- 字体优化
- 字体未下载完成时,浏览器隐藏或自动降级,导致字体闪烁
- font-display
- @font-face 字符集拆分
- ajax + base64
图片格式选择
JPEG
非常适合: 颜色丰富的照片,彩色图大焦点图、通栏 banner 图,结构不规则的图形
不适合:线条图形和文字、图标图形,因为它的压缩算法不太适合这些类型的图形,并且不支持透明度
压缩工具:jpegtran
PNG
非常适合: 纯色、透明、线条绘图、图标;边缘清晰、有大块相同颜色区域;颜色数较少单需要半透明
不适合:由于是无损存储,彩色图像体积太大,所以不太适合颜色丰富,彩色大图
压缩工具: node-pngquant-native
GIF
非常适合:动画,图标
不适合:每个像素只有 8 比特,不适合存储彩色图片
压缩工具:Gifsicle
Webp
非常适合:适用于图形和半透明图像
不适合: 最多处理 256 色,不适合彩色图片
真的需要图片吗?
Web font 代替图片
使用 Data URI 代替图片
采用 Image spriting(雪碧图)
渲染优化
- 避免回流与重绘,详细请看重绘与回流
- 避免布局抖动(FastDom防止布局抖动利器)
- 将动画和经常变化的元素提到单独的图层
代码优化
JS 优化
- Code splitting 代码拆分,按需加载
- Tree shaking 代码减重
- Scope Hoisting 模块合并
- 去除 console
- 避免长任务
- 避免超过 1kb 的行间脚本
- 大量事件绑定使用事件委托
- 适时使用防抖和节流
- 避免内存泄漏
- 使用 rAF 和 rIC 进行时间调度(react时间调度原理)
- 配合 V8 有效优化代码
- 源码 -> 抽象语法树 -> 字节码Bytecode -> 机器码
- 编译过程会进行优化(脚本流、字节码缓存、懒解析等)
- 运行时可能会发生反优化
- 对象优化可以做哪些
- 以相同顺序初始化对象成员,避免隐藏类的调整
- 实例化后避免添加新属性
- 尽量使用 Array 代替 array-like 对象
- 避免读取超过数组的长度
- 避免元素类型转换
HTML 优化
- 精简 HTML 代码
- 减少 iframes 使用,延迟加载 iframe,父文档加载完毕后再给 iframe 的 src 赋值
- 压缩空白符
- 避免节点深层级嵌套
- 避免 table 布局
- 删除注释
- CSS&Javascript尽量外链
- 删除元素默认属性
CSS 优化
提升 CSS 渲染性能
- 谨慎使用 expensive 昂贵的属性
- 如 :nth-child 伪类; position: fixed 定位
- 尽量减少样式层级数量
- 如 div ul li span i {color: true;}
- 尽量避免使用占用过多 CPU 和内存的属性
- 如 text-indent: -9999px
- 尽量避免使用耗电量大的属性
- 如 CSS3:3D transforms、CSS3 transitions、Opacity
- 尽量避免使用 CSS 表达式
- background-color: expression((new Date()).getHours() % 2 ? “#FFF” : “#000”)
- 尽量避免使用通配选择器,有选择地使用选择器
- body > a {font-weight: blod;}
- 尽量避免类正则的属性选择器
- *=,|=,^=,$=
- 使用 contain 属性,告诉浏览器某些方面可以这样优化,哪些不能优化
- 使用 font-display 属性,帮助我们把文字更早显示在页面上,还可以减少文字闪动问题
提升 CSS 文件加载性能
- 内联首屏关键CSS(Critical CSS)
- 使用外链的 CSS
- 尽量避免使用 @import
- 异步加载CSS
精简 CSS 代码
- 使用后缩写语句
- 删除不必要的零
- 删除不必要的单位,如px
- 删除过多的分号
- 删除空格的注释
- 尽量减少样式表的大小
合理使用 Web Fonts
- 将字体部署在 CDN 上
- 将字体以 base64 形式保存在 CSS 中并通过 localStorage 进行缓存
- Google 字体库因为某些不可抗原因,应该使用国内托管服务
CSS 动画优化
- 尽量避免同时动画
- 延迟动画初始化
- 结合 SVG
构建优化
加快构建速度(打包速度)
使用 speed-measure-webpack-plugin 插件可以测量各个插件和loader所花费的时间,量化打包速度,判断优化效果
- 缩小文件的搜索范围(配置include/exclude resolve.modules resolve.mainFields alias noParse extensions)
- 通过 exclude、include 配置来确保转译尽可能少的文件
- 优化 resolve.modules 配置
- 优化 resolve.mainFields 配置
- alias
- noParse
- extensions
- 在一些性能开销较大的 loader 之前添加 cache-loader,将结果缓存中磁盘中
- 使用 happypack 开启多进程打包
- 除了使用 Happypack 外,我们也可以使用 thread-loader 开启多进程打包 loader
- 使用 HardSourceWebpackPlugin 为模块提供中间缓存,第二次构建可大量节约时间
- 使用 IgnorePlugin 忽略第三方包指定目录,例如 moment 的本地语言包
- 使用 webpack-parallel-uglify-plugin 开启 JS 多进程压缩
减少打包文件体积
引入 webpack-bundle-analyzer 分析打包后的文件,判断哪些包还可以拆分和优化
- 使用 externals 配置,然后将 JS 文件、CSS 文件和存储在 CDN
- 使用 DllPlugin(动态链接库)将 bundles 拆分,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间
- 使用 optimization.splitChunks 配置抽离公共代码
- 使用 IgnorePlugin 忽略第三方包指定目录,例如 moment 的本地语言包(重复)
- 使用 url-loader 或 image-webpack-loader 对图片进行转化或者压缩处理
- 优化 SourceMap,开发环境推荐: cheap-module-eval-source-map,生产环境推荐: cheap-module-source-map
- 按需加载,项目中的路由懒加载
- webpack自身的优化:
- tree-shaking,在生产环境下,会自动移除没有使用到的代码
- scope hosting 作用域提升,变量提升,可以减少一些变量声明
- babel 配置的优化,配置 @babel/plugin-transform-runtime,重复使用 Babel 注入的帮助程序,以节省代码大小的插件
网络优化——Tcp和UDP相关优化
TCP优化建议
要对TCP进行优化,必须要最大限度地利用底层协议的原理,其原理性的东西无非就是以下的几点:
-
三次握手就是一次往返时间
-
慢启动在每个连接中都应用
-
流量控制和拥塞控制会影响到所有连接的吞吐量
-
吞吐量由当前拥塞窗口大小控制
服务器调优
-
增大TCP的初始化拥塞窗口
-
慢启动重启
-
窗口缩放
-
TCP快速打开
通过进行服务器的最优调整,把主机的操作系统升级到最新版本,可以确保每个 TCP 连接都具有较低的延迟和较高的吞吐量。
应用程序行为调优
-
数据能不发就不发
-
使用CDN让传输距离变短
-
复用TCP连接
请求的影响因素就是减少请求与压缩体积,通过减少一些不必要的数据传输和减少传输距离,能够使应用程序的行为最优。
总结
-
升级服务器内核版本: 将服务器升级到最新版本,TCP 的最佳实践以及影响其性能的底层算法一直在与时俱进,而且大多数变化都只在最新内核中才有实现
-
拥塞窗口大小为 10:增大 TCP 的初始化拥塞窗口(cwnd),这样TCP一次往返数据就较多,速度提升明显,特别是短暂链接
-
减少慢启动重启,禁用空闲后的慢启动
-
确保启动窗口缩放:开放窗口缩放,增大最大接收窗口(rwnd)大小,提高吞吐量
-
减少传输冗余数据
-
压缩传输的数据
-
服务器放到离用户最近的地方(CDN),减少网络延时
-
重用 TCP 连接:尽可能重用已经建立的TCP链接,减少三次握手,慢启动,拥塞控制对性能的影响
-
如果客户端和服务端都支持TFO(TCP fast open),则可以在三次握手的第个 SYN 分组中发送数据。
-
减少 HTTP 重定向,减少 DNS 查找
-
缓存资源,避免多次请求相同的内容
UDP的优化
由于 UDP 是一种简单的协议,它的高效性正是因为它忽略了很多 TCP 的特性,但是由于这样的高效性,可能也会造成麻烦。举个例子来说,当你看视频的时候假如没有经过拥塞处理,可能会占用大量的带宽,导致一些正常的 TCP 连接无法发送正常的数据,如网页也可能无法打开。另一种情况也有可能造成视频一直卡顿,无法加载。
所以我们针对这种情况必须进行有效的处理,根据 RFC 的文档,主要有几种优化方案:
-
控制传输速度
-
对所有的流量进行拥塞控制
-
使用与 TCP 相近的带宽
-
处理数据包丢失、重复和重排
vue 和 react 相关优化
vue 性能优化
- 引入生产环境的 Vue 文件
- v-if 和 v-show 选择调用
- 为 item 设置唯一 key 值
- 细分 vuejs 组件
- 减少 watch 的数据,watch 对象的时候使用对象字符串
- 内容类系统的图片资源按需加载(v-lazy、滚动到可视区域加载等)
- 单独添加的监听事件是不会移除的,需要手动移除事件的监听,以免造成内存泄漏
- 长列表性能优化(在大量数据展示的情况下,禁止 Vue 来劫持我们的数据能够很明显的减少组件初始化的时间,那如何禁止 Vue 劫持我们的数据?通过 Object.freeze 方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了)
- 扁平化 Store 数据结构,合理使用持久化 Store 数据
- 路由懒加载
- SSR(服务端渲染)
react 性能优化
这里只说 react 单独的进行的性能优化:
- key
- shouldComponentUpdate
- pureComponent
- 关于箭头函数,先声明好事件监听函数后,然后再拿到其引用传给组件:
- useCallback(大计算量的函数来)
- useMemo
- React.Memo
- 不可变数据 Immutable
- reselect
- React.lazy 按需加载
如果一定要做性能优化,核心还是在减少频繁计算和渲染上,在实现策略上主要有三种方式:利用key维持组件结构稳定性、优化数据比对过程和按需加载。其中优化数据比对过程可以根据具体使用的场景,分别使用缓存数据或组件、改用Immutable不可变数据等方式进行。最后,也一定记得要采用测试工具进行前后性能对比,来保障优化工作的有效性。
更多流行优化技术
- 移动端使用 SVG 图标
- 使用 Flexbox 优化布局
- 优化资源加载顺序,给资源设置优先级
- Preload:提前加载较晚出现,但对当前页面非常重要的资源
- Prefetch:提前加载后继路由需要的资源,优先级低
- dns-prefetch:对链接进行 DNS 预解析
- prerender:标识下一个导航可能需要的资源
- 预渲染页面(react-snap,vue也可以使用)
- Windowing、虚拟列表等提高列表性能
- 使用骨架组件减少布局移动和页面抖动
不同的应用场景可能会采用不同的优化方案和优化技术,对于一个应用,不能所有的优化都一股脑想用上去,因为优化也会带来一定的成本,比如配置麻烦,需要遵守一些特定的规则等等。
以上为自己的整理和总结,感谢阅读指正~
更多文章和精选面试题请看:chroot 的面试笔记