最近想系统的学习一下前端性能优化的知识, 查了很多资料, 结合自己的实际经历, 梳理了一下~
本文内容:
- 浏览器渲染过程
- 如何针对渲染过程进行优化?
CRP关键渲染路径(critical render path)
这是本章性能优化的关键概念, 我将会围绕着渲染的机制和步骤, 详细地阐述每一步的优化, 依次提高页面的渲染速度和性能.
1. 基于HTTP网络从服务器请求回来数据:
请求CSS也是按照这个流程
- 16进制的文件流
- 浏览器将数据解析为HTML字符串
- 按照W3C规则识别为节点
- 生成DOM树
2. 访问页面, 请求回来的是HTML文档, 浏览器自上而下渲染,服务器会开辟多个线程去执行
- GUI渲染线程: 渲染页面
- JS引擎线程: 渲染Javascript代码
- HTTP线程: 可以开辟多个, 从服务器拉取资源数据
- DOM监听线程
3. 页面渲染过程
CSS处理
-
遇到Style内嵌样式, 直接进行渲染
- 所以当
CSS
代码较少的时候, 可以直接使用内嵌样式, 因为HTML拉取完毕, 则CSS同时拉取完毕, 且同时渲染 - 但是, 代码较大的时候, 可能会影响HTML的拉取速度, 而且不利于代码维护, 此时用外链的方式会更好
- 所以当
-
遇到link的外联样式, 浏览器会创建一个HTTP线程, 去异步地请求资源文件信息, GUI线程则继续渲染
- 但是HTTP线程是有限的, 如: 谷歌浏览器并发数为(5-7个),如果超过了HTTP请求的最大并发数, 则需要排队(HTTP请求一定是越少越好)
-
遇到@import, 浏览器同样会开辟HTTP线程请求资源文件信息, 但是GUI线程会被阻塞, 它必须等待资源请求完毕才能继续渲染
- 真实项目中一定要避免使用@improt
一般会把link链接放在页面头部, 这是为了在渲染DOM的同时, 就让HTTP线程去服务器拉去CSS, 当DOM渲染完毕, CSS也拉取完了. 这样可以通过并发处理, 更有效地利用时间, 提高页面的渲染速度.
JavaScript代码处理:
应该把JavaScript代码放在页面底部, 防止其阻碍GUI渲染线程, 或者给script标签, 加上
defer
或async
-
遇到<script>标签, 会阻塞GUI渲染
如遇到
async
属性- 浏览器会去请求JS资源, GUI继续渲染, 但是 一旦资源请求完毕, GUI会立马阻塞, 加载JS代码
遇到
defer
属性- 浏览器请求js资源, GUI继续渲染, 等到整个DOM结构加载完毕, 再回来加载JS代码
总结: 浏览器的渲染流程
DOM加载完毕后, 假设CSS还未请求完毕, 这时是有可能会去加载JavaScript代码的
![]()
-
处理HTML标记, 构建DOM树
-
处理CSS标记, 构建CSSOM树
-
将DOM树和CSSOM树合成 RENDER 树
-
根据生成的渲染树, 计算他们在视口(viewport)内确切位置的大小, 这个计算阶段叫布局(Layout)
-
根据渲染树以及回流的几何信息, 得到节点的绝对像素, 对页面进行绘制(painting)
我们可以看出, 在阶段4如果对布局进行改变, 就必须向下执行阶段5, 这也是为什么, 回流一定会触发重绘, 但重绘不一定触发回流的原因
绘制阶段是分层绘制的, 我们可以通过京东网来看看分层绘制的好处.
打开流程 1. Chorme
F12
=> 点右边上交三个点点 => more tool => Layer
由上图我们可以看出, 这些层级实际上就是我们常说的脱离文档流, 这样做的好处就在于, 我们对这些脱离文档流的DOM元素
进行操作, 它导致它所在的那个层级出发回流机制.
优化方案
- 避免标签的深层次嵌套, 尽可能地语义化标签
- 尽快将CSS代码拉取到客户端
- 避免JavaScript代码阻塞渲染
- 减少回流和重绘
什么操作会引发回流 (Reflow):
-
页面首次渲染
-
DOM元素的尺寸和位置发生改变
-
元素的内容发生改变(文本改变, 加入一张尺寸不同的图片)
-
增加类名
-
操作DOM(添加或者删除可见的DOM)
-
获取DOM元素的视口位置宽度长度等属性(clientWidth、clientHeight、clientTop、clientLeft offsetWidth)
总结: 很明显对于会影响DOM元素大小,结构的操作,还有获取DOM元素大小信息的操作都会出发回流.(为了获取最准确的信息, 浏览器会回流一次, 以保证信息的准确性)
对于第四点,我们就有优化的思路了, 核心在于: "尽可能地减少浏览器触发回流"
我们能做的
第1点: 页面首次渲染(这个莫得办法)
第2点: 我们要尽可能让所有类似操作在一次回流中完成
第3, 4点:
- 对于样式的改变, 也要尽可能地集中完成如:
[DOM].style.cssText = "width:300px;height:200px;border:1px solid red;"
.box {
width:300px;
height:200px;
border:1px solid red;
}
[DOM].style.className = 'box'
第5点:
- 放弃传统操作DOM的时代, 基于Vue/React通过数据影响视图
- 元素批量修改(文档碎片)
let box = document.querySelector('#box')
let frag = document.createDocumentFragment()
for(leti = 0; i < 10; i++){
let span = document.createElement('span')
frag.append(span)
}
box.append(frag)
第6点:
- 缓存我们获取的样式信息
let boxWidth = box.offsetLeft // 保存起来, 避免每次用都重新获取
其他:
-
具有动画效果的DOM元素,要脱离文档流(
absolute
/fiex
) -
CSS硬件加速(GPU)
transform
/filter
/opacity
: 使用这些属性可以让元素脱离出一个单独的层, 并进行硬件加速, 如transfrom
做动画, 它比用left
等属性的性能更好, 既有硬件加速, 又不会导致回流- 或者给元素添加一个
transform: translateX(0)
同样的能够欺骗浏览器对某一个元素采取硬件加速, 但显然这样滥用内存不是一个好的做法.
-
避免使用table布局和CSS的JavaScript表达式
浏览器自动优化策略
对于浏览器来说, 经常每执行一行代码, 就要做一次回流, 是非常耗能的事情, 所以一般都是把所有的操作放到一个队列中, 集中渲染
渲染队列
刷新渲染队列条件:
- 无修改样式代码
- 遇到获取元素样式代码
遇到上述两种情况, 渲染队列就会立即触发回流机制.
代码层面
webpack层面优化
用我以前的项目来做一下优化~
- CDN 引入
// vue.config.js
configureWebpack: {
externals: {
vue: 'Vue',
'vue-router': 'VueRouter',
},
}
// index.html
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script src="//cdn.bootcss.com/vue-router/2.5.3/vue-router.min.js"></script>
- 路由懒加载
const categoryEdit = () => import('@/views/CategoryEdit')
path: '/categories/create', component: categoryEdit
优化前:
优化后:
其中最大的几个模块, 我都用CDN引入, 不得不说效果真的震惊..
![]()
感谢😘
如果觉得文章内容对你有帮助:
- ❤️欢迎关注点赞哦! 我会尽最大努力产出高质量的文章
个人公众号: 前端Link
联系作者: linkcyd 😁
往期: - 作用域与闭包: 最坑的面试题, 你做得对吗?
- 原型与原型链: 如何自己实现 call, bind, new?
- 数据类型检测: 面试官能问的都在这里啦