让前端飞JavaScript%……%

Web前端性能优化(二)

2018-12-04  本文已影响9人  Nian糕
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,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,所以回流必将引起重绘,而重绘不一定会引起回流

触发重绘的相关属性有 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. 优化

// 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,之后的例子同学们可自行查看运行耗时,就不再逐个展示

// 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>
#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>
#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>
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)
}
End of File

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

上一篇下一篇

猜你喜欢

热点阅读