前端性能优化

实践中性能优化的全过程

发现性能问题

发现项目的某个页面在加载的时候出现卡顿

性能测试

  • 浏览器工具的performance、lighthouse
  • 发现性能指标存在问题:比如FCP、TTI过长

  • FCP:页面上第一个内容元素首次呈现在用户屏幕上的时间
  • TTI:用户可以与页面上的元素进行交互的时间

问题分析

  • 页面加载时会先获取较大的图片,导致FCP指标过长

优化方案

  • 使用图片懒加载技术

量化优化指标

  • 结合性能指标,没有量化的性能指标都是耍流氓
  • 终将FCP优化到了1.8秒,TTI优化到了3.8秒

优化是否达标

  • 性能指标的要求:FCP必须在2秒以类等等

系统的优化方案

项目构建时(开发时)性能优化

  • 这一步主要是构建打包方面的问题,和vite、webpack是强耦合的
  • 主要思路是并行缓存
    • 打包缓存:比如umi的MFSU或者hardsource-webpack-plugin等工具都可以大大减少构建时间
    • 并行构建:释放CPU多核并发的优势。诸如happyPack、thread-loader 等工具都可以在不同阶段开启CPU多核进行并行构建,大大提升开发时效率。
    • 等等

页面(生产时)性能优化

浏览器原理(从输入url到看到界面的过程)

  • 页面性能优化的一系列操作都是围绕着浏览器原理来展开的
  • 主要步骤
  1. 检查缓存:如果浏览器有本地的静态资源文件缓存,并且未过期,则直接从缓存中读取,而不会发送网络请求
  2. DNS解析:将输入的url对应的域名解析成ip
    • DNS解析查找过程: 本机的hosts文件->本地DNS缓存->本地域名服务器->根域名服务器查找->顶级域名服务器查找->权威域名服务器查找。最终获得IP地址后,本地DNS服务器将IP地址返回给请求的用户
    • 用户向本地DNS服务器发起请求属于递归请求,本地DNS服务器向各级域名服务器发起请求属于迭代请求
  3. TCP三次握手建立连接
  4. 发送HTTP请求
  5. 返回数据: 当页面请求发送到服务器端后,服务器端会返回一个 html 文件作为响应,浏览器接收到响应后,开始对 html 文件进行解析,开始页面的渲染过程。
  6. 浏览器解析渲染页面
    在这里插入图片描述
    解析过程:
    1. 首先解析HTML构建DOM树
    2. 碰到css的link标签下载解析css生成CSSOM树,不会影响DOM解析,但是会阻塞Render树的构建过程
    3. 碰到script标签,会下载解析js脚本。判断是否有defer和async属性,没有的话会造成页面渲染的阻塞,会停止解析HTML构建DOM树
    4. 将DOM树和CSSOM结合构建Render树
    5. 布局:计算渲染树上每个节点的几何体,确定所有节点的宽高和位置信息
    6. 渲染:将每个节点绘制到屏慕上
  7. TCP四次挥手断开连接

在浏览器渲染页面过程中的细节:
渲染树用于实际的页面渲染,和DOM树不是一一对应的,渲染树中不存在DOM树中隐藏(display:none)的元素
CSS解析会阻塞渲染,因为构建渲染树需要CSSOM
JS下载解析会阻塞DOM树的构建
defer的JS会异步下载,在DOM树构建之后,DOMContentLoaded事件(一些外部资源如图片等还没加载完成)之前执行脚本,不会阻塞HTML解析
async的js会异步下载,下载完成后执行,即即下载不阻塞HTML解析,但执行阻塞HTML解析

加载时性能优化

加载时即html文件的网络请求过程

  • 优化思路:
    • 文件体积小
    • 网络快,优化网络请求速度或次数
    • 缓存
构建策略:减少文件体积
  • webpack的代码拆分
  • 按需加载,动态导入
  • 压缩资源:压缩html/js/css代码体积
  • 图像压缩
网络策略
  • 使用CDN
  • 减少重定向
  • DNS预解析
  • 避免图片src为空
缓存
  • 强缓存
  • 协商缓存

渲染时性能优化

浏览器渲染的过程中的优化就落实到很多我们前端的具体技术细节上了

CSS策略

CSS规则优化:减少浏览器匹配元素和计算值的时间

  • 避免出现多层嵌套
  • 避免为id选择器添加多于选择器(因为ID选择器已经是具有高优先级的选择器)
  • 避免使用通配符选择器
DOM策略

先理解回流和重绘

  • 回流一定会引起重绘,回流是十分消耗性能的
  • 所以避免回流和重绘也是渲染时优化的重要部分

回流

当渲染树中部分或者全部元素的大小、位置、布局发生改变时,浏览器重新渲染部分或者全部文档的过程称为回流

会导致回流的操作:

  • 比如DOM结构发生变化(添加新的节点或者移除节点)
  • 比如改变了布局(修改了width、height、padding、font-size等值)
  • 比如窗口resize(修改了窗口的尺寸等)
  • 比如调用getComputedStyle方法
  • 获取尺寸、位置等信息

注意,多次修改DOM结构不一定会触发多次回流

document.getElementById('root').stlye.width = '100px';
document.getElementById('root').stlye.height = '100px';
document.getElementById('root').stlye.top = '10px';
document.getElementById('root').stlye.left = '10px';

上面代码是写操作,只会触发一次回流,这是因为浏览器的优化机制

但是获取offset等元素属性,每获取一次都会触发一次回流,这是因为offset等属性,要会及时回流完才能拿到最新的值

如何减少重排

  1. 避免元素影响到所在文档流
    使用**绝对定位(position:absolute)**使元素脱离文档流,这样元 素的变化不会导致其他元素的布局变化,也就不会引起重排

    如果使用CSS的transform属性实现动画,则不需要重排和重绘,直接在合成线程合成动画操作

  2. 读写分离

  • 浏览器对写操作会采用渲染队列机制,将写操作放入异步渲染队列,异步批量执行。
  • 当JS遇到读操作时候(offset / scroll / client),会把异步队列中相关的操作提前执行,以便获取到准确的值。
div.style.left = '10px';
console.log(div.offsetLeft);
div.style.top = '10px';
console.log(div.offsetTop);
div.style.width = '20px';
console.log(div.offsetWidth);
div.style.height = '20px';
console.log(div.offsetHeight);

上面代码如果没有console.log的读操作,执行后,浏览器并不会触发4次重排,而是会将3个操作放入一个渲染队列中,异步批量执行,因此可能只会触发一次重排。

当遇到读操作时候,则立刻执行渲染队列中相关操作,从而马上触发重排。

上面每个console.log()都会让浏览器取出异步渲染队列中的写操作执行,然后返回重排后的值。

对于上面的情况,为了避免重排,应该进行读写分离

div.style.left = '10px';
div.style.top = '10px';
div.style.width = '20px';
div.style.height = '20px';
console.log(div.offsetLeft);
console.log(div.offsetTop);
console.log(div.offsetWidth);
console.log(div.offsetHeight);

上面代码在执行console.log()的时候,浏览器把之前上面四个写操作的渲染队列都给清空了。因为渲染队列是空的,所以后面的读操作并没有触发重排,仅仅取值而已。

如果需要根据当前的样式设置新样式,应该先缓存当前样式,然后批量更新样式。

// bad 强制刷新,触发两次重排
div.style.left = div.offsetLeft + 1 + 'px';
div.style.top = div.offsetTop + 1 + 'px';

// good 缓存布局信息,读写分离
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
div.style.left = curLeft + 1 + 'px';
div.style.top = curTop + 1 + 'px';
  1. 集中改变样式
    虽然浏览器有异步渲染队列的机制,但是异步flush的时机我们没有办法控制,为了保证性能,还是应该集中改变样式。
// bad
var left = 10;
var top = 10;
el.style.left = left + "px";
el.style.top  = top  + "px";

// good 
el.className += " theclassname";
// good
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
  1. 离线操作DOM
    如果需要进行多个DOM操作(添加、删除、修改),不要在当前的DOM中连续操作(如循环插入li)。

在要操作DOM之前,通过display隐藏DOM,当操作完成之后,才将元素的display属性为可见,因为不可见的元素不会触发重排和重绘。

dom.display = 'none';

// 执行DOM操作...

dom.display = 'block';
  • 通过使用DocumentFragment缓存批量化DOM操作,在它上面批量操作DOM,操作完成之后,再添加到文档中,这样只会触发一次重排。
  • 复制节点,在副本上操作,然后替换原节点。

参考原文

重绘

浏览器根据元素的样式发生改变而重新绘制元素的过程,但不会影响布局

会导致重绘的操作:

  • 比如修改背景色、文字颜色、边框颜色、样式等
阻塞策略
  • 脚本与DOM/其它脚本的依赖关系很强:对script设置defer
  • 脚本与DOM/其它脚本的依赖关系不强:对script设置async
代码实践策略
  • 防抖、节流
  • 懒加载
  • 绘图时可开启GPU加速
  • 时间分片、Web Worker处理大、长逻辑
  • 28
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值