Web前端性能优化(二)

Unsplash

1. 懒加载和预加载

懒加载 即延迟加载,在电商或是页面很长的业务场景中,我们通常会使用懒加载的方式对图片进行请求,只有在图片进入可视区域之后才请求图片资源,而在之前都通过一张占位图进行占位,将真正的图片路径存储在元素的 data-url 中,这样做的好处在于减少无效资源的加载,并不是所有的用户都会浏览完网站的所有图片,而且浏览器是存在并发上限的,并发加载的资源过多会阻塞 JS 的加载,影响网站的正常使用

手淘懒加载实例

懒加载具体效果可自行通过下面代码实现,也可以使用 zepto.lazyload 插件或 vue-lazyload 插件

<img src='' class='image-item' lazyload='true' data-original='http://upload-images.jianshu.io/upload_images/1662958-e1f38db94deaddd1.jpg'>
<img src='' class='image-item' lazyload='true' data-original='http://upload-images.jianshu.io/upload_images/1662958-f3dd943e438d31f6.jpg'>
<img src='' class='image-item' lazyload='true' data-original='http://upload-images.jianshu.io/upload_images/1662958-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:"http://upload-images.jianshu.io/upload_images/1662958-5684e095da8b8a2b.jpg"},
    {id: "myImage", src:"http://upload-images.jianshu.io/upload_images/1662958-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> 元素不断的在重绘

重绘元素_1

重绘元素_2

Chrome 中的 Layer 工具可查看图层数量,将全局 DOM 元素设置 transform:translateZ(0);will-change: transform; 属性,将其变成新的独立图层,而每一个图层会消耗大量的时间和运算量,直接导致了页面崩溃

Layer 图层_1

Layer 图层_2

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 替代 visibilityvisibility 会不断触发重绘过程
// 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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券