Web前端性能优化(二)
1. 懒加载和预加载
懒加载 即延迟加载,在电商或是页面很长的业务场景中,我们通常会使用懒加载的方式对图片进行请求,只有在图片进入可视区域之后才请求图片资源,而在之前都通过一张占位图进行占位,将真正的图片路径存储在元素的 data-url
中,这样做的好处在于减少无效资源的加载,并不是所有的用户都会浏览完网站的所有图片,而且浏览器是存在并发上限的,并发加载的资源过多会阻塞 JS 的加载,影响网站的正常使用
懒加载具体效果可自行通过下面代码实现,也可以使用 zepto.lazyload 插件或 vue-lazyload 插件
<img src='' class='image-item' lazyload='true' data-original='https://img.haomeiwen.com/i1662958/e1f38db94deaddd1.jpg'>
<img src='' class='image-item' lazyload='true' data-original='https://img.haomeiwen.com/i1662958/f3dd943e438d31f6.jpg'>
<img src='' class='image-item' lazyload='true' data-original='https://img.haomeiwen.com/i1662958/5684e095da8b8a2b.jpg'>
var viewHeight = document.documentElement.clientHeight // 可视区域的高度
function lazyload() {
var eles = document.querySelectorAll('img[data-original][lazyload]')
Array.prototype.forEach.call(eles, function(item, index) {
var rect
if(item.dataset.original === '')
return
rect = item.getBoundingClientRect()
if(rect.bottom >= 0 && rect.top < viewHeight) {
!function() {
var img = new Image()
img.src = item.dataset.original
img.onload = function() {
item.src = img.src
}
item.removeAttrbute('data-original')
item.removeAttrbute('lazyload')
}()
}
})
}
lazyload() // 首屏尚未触发 scroll 事件,需要手动去触发该事件进行图片加载
document.addEventListener('scroll', lazyload)
预加载 即在图片等静态资源在使用之前提前请求,当资源使用时直接从本地缓存中加载,提升用户体验,适用于页面需要资源相互依赖的场景,如 H5 动画
京东招聘预加载实例预加载主要有 3 种方式,① 使用 display:none;
将图片请求下来但并不显示,通过脚本进行控制显示/隐藏;② 使用 Image 对象,通过 new Image()
的方式创建一个图片对象,通过 JS 给图片 src
属性进行赋值;③ 使用 XMLHttpRequest 对象,其优点在于能更加精细的控制预加载过程,但缺点在于,可能会出现跨域问题
若是想对跨域可能性进行兼容,推荐大家使用 PreloadJS 模块
var queue = new createjs.LoadQueue(false); // 使用 html 方式进行预加载
queue.on("complete", handleComplete, this);
queue.loadManifest([
{id: "myImage", src:"https://img.haomeiwen.com/i1662958/5684e095da8b8a2b.jpg"},
{id: "myImage", src:"https://img.haomeiwen.com/i1662958/f3dd943e438d31f6.jpg"}
]);
function handleComplete() {
var image = queue.getResult("myImage");
document.body.appendChild(image);
}
2. 重绘与回流
在浏览器中,JS 引擎和 UI 是在单独线程中工作的,有一个线程负责进行 JS 的解析,还有一个线程负责 UI 渲染,JS 在某些场景下会获取渲染的结果,若 JS 线程和 UI 线程是在并行执行的,那有可能获取不到我们预期的结果,所以这两个线程是互斥的,当一个线程在解析或渲染时,另一个线程则被冻结,所以我们就能够知道 CSS 的性能会让 JS 变慢, 而频繁的触发重绘与回流,会导致 UI 频繁渲染,最终导致 JS 变慢
当 Render Tree 中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建,这就称为 回流 Reflow,当 Render Tree 中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,就称为 重绘 Repaint,在回流的时候,浏览器会使 Render Tree 中受到影响的部分失效,并重新构造这部分 Render Tree,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,所以回流必将引起重绘,而重绘不一定会引起回流
- 盒子模型相关属性会触发重布局
width
,height
,padding
,margin
,display
,border-width
,border
,min-height
- 定位属性及浮动也会触发重布局
top
,bottom
,left
,right
,position
,float
,clear
- 改变节点内部文字结构也会触发重布局
text-align
,overflow-y
,font-weight
,overflow
,font-family
,line-height
,vertival-align
,white-space
,font-size
触发重绘的相关属性有 color
, border-style
, border-radius
, visibility
, text-decoration
, background
, background-image
, background-position
, background-repeat
, background-size outline-color
, outline
, outline-style
, outline-width
, box-shadow
我们通过 Chrome 的 Performance 工具,记录手淘 tab 图切换时,页面的重绘回流过程
手淘重绘回流实例新建 DOM 的过程:① 获取 DOM 后分割为多个图层;② 对每个图层的节点计算样式结果 Recalculate style 样式重计算;③ 为每个节点生成图形和位置 Layout 回流和重布局;④ 将每个节点绘制填充到图层位图中 Paint Setup 和 Paint 重绘;⑤ 图层作为纹理上传至 GPU;⑥ 符合多个图层到页面上生成最终屏幕图像 Composite Layers 图层重组
在图像层面,我们可以局限重绘回流的范围,将不断重绘或消耗大量运算量的 DOM 元素独立为一个图层,在 Chrome 的 Rendering 工具中勾选 Paint flashing 选项,拖动窗口大小,可以看到重绘的元素被标志为绿色,而 <video>
元素不断的在重绘
Chrome 中的 Layer 工具可查看图层数量,将全局 DOM 元素设置 transform:translateZ(0);
或 will-change: transform;
属性,将其变成新的独立图层,而每一个图层会消耗大量的时间和运算量,直接导致了页面崩溃
Chrome 创建图层的条件有:① 3D 或透视变换 CSS 属性 (perspective transform); ② 使用加速视频解码的 <video>
节点; ③ 拥有 3D (WebGL) 上下文或加速的 2D 上下文的 <canvas>
节点; ④ 混合插件,如 Flash; ⑤ 对自己的 opacity
做 CSS 动画或使用一个动画 webkit 变换的元素; ⑥ 拥有加速 CSS 过滤器的元素; ⑦ 元素有一个包含复合层的后代节点(一个元素拥有一个子元素,该子元素在自己的层里); ⑧ 元素有一个 z-index
较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)
3. 优化
- 用
translate
替代top
改变,top
会触发 Layout 过程,translate
不会
// top
#rect {
position: relative;
top: 0;
width: 100px;
height: 100px;
background: lightcyan;
}
<div id="rect"></div>
<script>
setTimeout(() => {
document.getElementById('rect').style.top = '100px'
}, 2000)
</script>
运行结果_1
运行耗时_1
使用 top
共计耗时 56+55(Layout)+92+23+110=336us
// translate
#rect {
transform: translateY(0);
width: 100px;
height: 100px;
background: lightcyan;
}
<div id="rect"></div>
<script>
setTimeout(() => {
document.getElementById('rect').style.transform = 'translateY(100px)'
}, 2000)
</script>
运行结果_2
运行耗时_2
使用 translate
共计耗时 62+58+57=177us,之后的例子同学们可自行查看运行耗时,就不再逐个展示
- 用
opacity
替代visibility
,visibility
会不断触发重绘过程
// visibility
#rect {
width: 100px;
height: 100px;
background: lightcyan;
}
<div id="rect"></div>
<script>
setTimeout(() => {
document.getElementById('rect').style.visibility = 'hidden'
}, 2000)
</script>
rect
元素是位于 document
图层中的,当我们改变 rect
元素的阿尔法值时,是会影响到 rect
元素的兄弟元素的,虽然在当前例子中只有一个 rect
元素,但浏览器无法判断 document
图层是不是只有 rect
元素,所以我们需要将 rect
元素独立为一个新的图层
// opacity
#rect {
width: 100px;
height: 100px;
background: lightcyan;
opacity: 1;
transform: translateZ(0);
}
<div id="rect"></div>
<script>
setTimeout(() => {
document.getElementById('rect').style.opacity = '0'
}, 2000)
</script>
- 不要一条一条地修改 DOM 的样式,每修改一次 DOM 样式就会触发重绘,所以预先定义好 class,然后修改 DOM 的 className
#rect {
position: relative;
width: 100px;
height: 100px;
background: lightcyan;
opacity: 1;
}
<div id="rect"></div>
<script>
setTimeout(() => {
document.getElementById('rect').style.width = '200px'
document.getElementById('rect').style.height = '300px'
document.getElementById('rect').style.left = '30px'
document.getElementById('rect').style.top = '20px'
}, 2000)
</script>
#rect {
position: relative;
width: 100px;
height: 100px;
background: lightcyan;
opacity: 1;
}
#rect.active {
width: 200px;
height: 300px;
left: 30px;
top: 20px;
}
<div id="rect"></div>
<script>
setTimeout(() => {
document.getElementById('rect').className = 'active'
}, 2000)
</script>
- 将 DOM 离线后修改,如:先将 DOM 给
display:none
,此时会触发一次Reflow
,之后进行的样式修改都不会触发重绘回流,修改完毕后再把它显示出来
#rect {
position: relative;
width: 100px;
height: 100px;
background: lightcyan;
opacity: 1;
display: none;
}
<div id="rect"></div>
<script>
setTimeout(() => {
document.getElementById('rect').style.opacity = '0'
document.getElementById('rect').width = '200px'
document.getElementById('rect').height = '300px'
document.getElementById('rect').left = '30px'
document.getElementById('rect').top = '20px'
document.getElementById('rect').opacity = '1'
document.getElementById('rect').display = 'block'
}, 2000)
</script>
- 不要把 DOM 节点的属性值放在一个循环里当成循环里的变量,如
offsetHeight
,offsetWidth
var doms = [] // 通过选择器选择出一个dom元素的数组
var domsTop = []
// 根据当前页面的可视区域的高度,去计算这个dom元素的位置
for (var i = 0; i < doms.length; i++) {
domsTop.push(document.body.clientHeight + i * 100)
}
var doms = [] // 通过选择器选择出一个dom元素的数组
var domsTop = []
// 根据当前页面的可视区域的高度,去计算这个dom元素的位置
var clientHeight = document.body.clientHeight
for (var i = 0; i < doms.length; i++) {
domsTop.push(clientHeight + i * 100)
}
-
不要使用 Table 布局,可能很小的一个小改动会造成整个 Table 的重新布局
-
动画实现的速度的选择,UI 的频繁渲染会导致 JS 变慢
-
对于动画新建图层,如
<video>
,<canvas>
及设置了transform:translateZ(0);
或will-change: transform;
属性的元素 -
启用 GPU 硬件加速,浏览器会检测节点中的某些 CSS 属性,如
transform: translateZ(0);
和transform: translate3d(0, 0, 0);
,当检测到这些 CSS 属性时,浏览器就会启用硬件加速
End of File
行文过程中出现错误或不妥之处在所难免,希望大家能够给予指正,以免误导更多人,最后,如果你觉得我的文章写的还不错,希望能够点一下喜欢和关注,为了我能早日成为简书优秀作者献上一发助攻吧,谢谢!^ ^