你要知道的前端性能优化总结

1,833 阅读13分钟

什么是前端性能优化?

通常来说我们说的前端性能优化是指:从用户开始访问我们的网站到整个页面完整地展现出来的过程中,通过各种优化策略和优化方法,让页面加载的更快,让用户的操作响应更及时,给用户更好的使用体验。

前端性能优化是我们每个前端打工人都必须面对的问题,那怎么进行性能优化呢?下面我就给大家分享我所了解的一些性能优化知识,有什么疑问请大家提出来,谢谢~

性能优化指标与测量工具

工欲善其事必先利其器,在进行性能优化之前,我们应该准备好自己的性能优化测量工具,并且要有优化目标,即某项或者多项指标需要优化到什么程度。

常用的性能优化指标

  • 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所花费的时间,量化打包速度,判断优化效果

  1. 缩小文件的搜索范围(配置include/exclude resolve.modules resolve.mainFields alias noParse extensions)
    1. 通过 exclude、include 配置来确保转译尽可能少的文件
    2. 优化 resolve.modules 配置
    3. 优化 resolve.mainFields 配置
    4. alias
    5. noParse
    6. extensions
  2. 在一些性能开销较大的 loader 之前添加 cache-loader,将结果缓存中磁盘中
  3. 使用 happypack 开启多进程打包
  4. 除了使用 Happypack 外,我们也可以使用 thread-loader 开启多进程打包 loader
  5. 使用 HardSourceWebpackPlugin 为模块提供中间缓存,第二次构建可大量节约时间
  6. 使用 IgnorePlugin 忽略第三方包指定目录,例如 moment 的本地语言包
  7. 使用 webpack-parallel-uglify-plugin 开启 JS 多进程压缩

减少打包文件体积

引入 webpack-bundle-analyzer 分析打包后的文件,判断哪些包还可以拆分和优化

  1. 使用 externals 配置,然后将 JS 文件、CSS 文件和存储在 CDN
  2. 使用 DllPlugin(动态链接库)将 bundles 拆分,使用 DllReferencePlugin(索引链接) 对 manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间
  3. 使用 optimization.splitChunks 配置抽离公共代码
  4. 使用 IgnorePlugin 忽略第三方包指定目录,例如 moment 的本地语言包(重复)
  5. 使用 url-loader 或 image-webpack-loader 对图片进行转化或者压缩处理
  6. 优化 SourceMap,开发环境推荐: cheap-module-eval-source-map,生产环境推荐: cheap-module-source-map
  7. 按需加载,项目中的路由懒加载
  8. webpack自身的优化:
    1. tree-shaking,在生产环境下,会自动移除没有使用到的代码
    2. scope hosting 作用域提升,变量提升,可以减少一些变量声明
    3. 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不可变数据等方式进行。最后,也一定记得要采用测试工具进行前后性能对比,来保障优化工作的有效性。

React性能优化小贴士

更多流行优化技术

  • 移动端使用 SVG 图标
  • 使用 Flexbox 优化布局
  • 优化资源加载顺序,给资源设置优先级
    • Preload:提前加载较晚出现,但对当前页面非常重要的资源
    • Prefetch:提前加载后继路由需要的资源,优先级低
    • dns-prefetch:对链接进行 DNS 预解析
    • prerender:标识下一个导航可能需要的资源
  • 预渲染页面(react-snap,vue也可以使用)
  • Windowing、虚拟列表等提高列表性能
  • 使用骨架组件减少布局移动和页面抖动

不同的应用场景可能会采用不同的优化方案和优化技术,对于一个应用,不能所有的优化都一股脑想用上去,因为优化也会带来一定的成本,比如配置麻烦,需要遵守一些特定的规则等等。

以上为自己的整理和总结,感谢阅读指正~

更多文章和精选面试题请看:chroot 的面试笔记