美文网首页让前端飞JavaScript%……%
Web前端性能优化(二)

Web前端性能优化(二)

作者: Nian糕 | 来源:发表于2018-12-04 09:07 被阅读9次
    Unsplash

    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> 元素不断的在重绘

    重绘元素_1 重绘元素_2

    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 较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)

    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

    行文过程中出现错误或不妥之处在所难免,希望大家能够给予指正,以免误导更多人,最后,如果你觉得我的文章写的还不错,希望能够点一下喜欢关注,为了我能早日成为简书优秀作者献上一发助攻吧,谢谢!^ ^

    相关文章

      网友评论

        本文标题:Web前端性能优化(二)

        本文链接:https://www.haomeiwen.com/subject/mzpyfqtx.html