微信号:QunarTL

介绍:Qunar技术沙龙是去哪儿网工程师小伙伴以及业界小伙伴们的学习交流平台.我们会分享Qunar和业界最前沿的热门技术趋势和话题;为中高端技术同学提供一个自由的技术交流和学习分享平台.

前端优化:懒加载思考

2019-02-26 09:30 杨帆

暗中观察


默默关注


杨帆,2018年加入去哪儿,很荣幸能和很多优秀的同事一起打拼,目前在大住宿/大前端/综合业务团队,主要负责专题系统的开发和优化等工作,涉及前端组件化开发和后端 node 工程的开发。希望在大前端架构思想上有所突破,帮助同事和团队在大前端领域里提高开发效率。

一、简介

在进行系统优化的时候,如果只局限于前端范畴的话,所能使用的方法很有限。通常能想到的就是如何让首屏更加流畅,提升用户体验。 懒加载指的是,合理的进行图片、静态资源延迟加载,甚至接口或业务逻辑的延迟执行,做到给首屏最大的性能空间,达到页面首屏更快的渲染效果。 这种思想,适合很多前端工程的优化场景。

二、实现思路

1、资源站位

在懒加载的时候,通常是需要跳过原本的执行步骤。比如:懒加载图片时,需要进行标签的站位,事先设定图片宽高,使用 data-* 属性设置好待请求的图片地址,这时,图片还没有被浏览器下载。

 
           
  1. <img class="qLazy" data-original='https://xyz.abc.com/common/logo.png' />

由于没有此时没有设置 src 属性,所以 data-original 地址对应的图片并不会进行加载。 如果直接在 src 属性设置好图片地址,浏览器解析到则会直接下载,那么,就失去了懒加载的意义。

2、监听 scroll 事件

在懒加载时,需要制定懒加载的容器。比如:某些可以滚动的内容列表区域。 但在实际开发中,往往不会显式的制定容器,举个例子:手机端常常会有长列表,随整个页面滚动展现,这时如果进行懒加载优化,默认滚动容器应该为 window 对象。

3、判断资源位置是否在视窗内

这个功能是懒加载功能的核心,因为,懒加载也可以称为延迟加载,但什么时候进行加载呢?一定是用户在看到或者即将要看到待加载资源的时候,提前进行加载。这样,用户在看到资源时,资源已经加载完毕,可以给用户更好体验。 借用网络上一张图片加以说明:

即:当绿色待加载的页面元素,即将进入蓝色可视区域或已经进入可视区域后,执行懒加载方法。

4、判断加载资源或执行相应的回调逻辑

这个功能可以是根据业务进行扩展或更改。懒加载最初的构想只设计在了图片标签上,因为大部分手机端页面 80% 左右都会是图片,能够合理的控制图片的加载就能更好的改善用户体验。但也有例外,随着前端的功能越来越强大,很多渲染工作都会交给前端负责,很多业务逻辑也都又前端进行控制。所以,系统会很希望扩展一些懒执行功能,保证首屏逻辑优先完成,后续渲染或复杂操作延迟执行。 下面是部分懒加载实现源码。

 
           
  1. function QLazyLoader(setting) {

  2.    var _setting = {

  3.        selector: '.qLazy',

  4.        // 事件名

  5.        event: 'scroll',

  6.        // 默认绑定 evnet 的对象容器

  7.        container: setting.container || window,

  8.        // 获取 src 的 data 属性,默认 data-attrbute

  9.        attribute: 'data-original',

  10.        // 设置懒加载类型 默认为img  选项: 图片:img 只执行回调:call

  11.        loadtype: 'img',

  12.        // 回调事件,元素出现在视口中 function

  13.        appear: null,

  14.        // 触发 load 事件时执行的回调 function

  15.        load: null

  16.    };


  17.    //省略:进行滚动监听

  18.    //省略:判断是否出现再视窗内

  19.    //如果元素出现在视窗内

  20.    elements.each(function () {

  21.        var dom = this;

  22.        var jqThis = $(dom);


  23.        var qSrc = jqThis.attr(setting.attribute);


  24.        var loadAction = function() {};


  25.        var loadedCall = function() {

  26.            elements = $(grepElements(elements));

  27.            if (setting.load) {

  28.                setting.load.call(dom, jqThis, elements.length, setting);

  29.            }

  30.        }


  31.        if (/img|background-image/.test(setting.loadtype) && qSrc) {

  32.            var _img = jqThis;

  33.            // 开始懒加载图片

  34.            if (!_img.attr('src')) {

  35.                jqThis.attr('src', qSrc);

  36.            }

  37.            _img.one('error', function () {

  38.                console.log('qSrc loaded error', qSrc);

  39.            });

  40.        }

  41.        if (/js/.test(setting.loadtype) && qSrc) {

  42.            loadAction = function() {

  43.                LoadScript(qSrc, loadedCall);

  44.            }

  45.        }

  46.        if (/css/.test(setting.loadtype) && qSrc) {

  47.            loadAction = function() {

  48.                LoadStyle(qSrc, loadedCall);

  49.            }

  50.        }

  51.        if (/call/.test(setting.loadtype)) {

  52.            loadAction = loadedCall;

  53.        }

  54.        jqThis.one(APPEAR_EVENT, function (e) {

  55.            if (!dom.loaded) {

  56.                if (setting.appear) {

  57.                    setting.appear.call(dom, $(this), elements.length, setting);

  58.                }

  59.                loadAction();

  60.            }

  61.        });

  62.    });

  63. }

三、如何判断资源位置是否出现在视窗内

1、根据可视区域高度和滚动高度以及元素距离页面顶端的距离进行计算

 
           
  1. function isElementInViewport (el) {

  2.    // 获取div距离顶部的偏移量

  3.    var offsetTop = el.offsetTop;

  4.    // 获取屏幕高度

  5.    var clientHeight = document.documentElement.clientHeight;

  6.    // 屏幕卷去的高度

  7.    var scrollTop = document.documentElement.scrollTop;

  8.    if( clientHeight + scrollTop > offsetTop ) {

  9.        console.log("已经进入可视区");

  10.    } else {

  11.        console.log("并没有进入可视区");

  12.    }

  13. }

注意:document.documentElement 的兼容性问题,不在此次讲解范围内。

2、getBoundingClientRect API

这个方法非常有用,常用于确定元素相对于视口的位置。该方法会返回一个 DOMRect 对象,包含 left,top,width,height,bottom,right 六个属性。 left,right,top,bottom: 都是元素(不包括 margin )相对于视口的原点(视口的上边界和左边界)的距离。

 
           
  1. // 用法举例:

  2. var ro = object.getBoundingClientRect();

  3. var Top = ro.top;

  4. var Bottom = ro.bottom;

  5. var Left = ro.left;

  6. var Right = ro.right;

  7. var Width = ro.width || Right - Left;

  8. var Height = ro.height || Bottom - Top;

 
           
  1. function isElementInViewport (el) {

  2.    var rect = el.getBoundingClientRect();

  3.    return (

  4.        rect.top >= 0 &&

  5.        rect.left >= 0 &&

  6.        rect.bottom <= (document.documentElement.clientWidth || document.documentElement.clientHeight) &&

  7.        rect.right <= (document.documentElement.clientWidth || document.documentElement.clientWidth)

  8.    );

  9. }

3、IntersectionObserver API

至于是否使用这个 API ,个人建议了解就好,目前仍处于 w3c 草案阶段 ,并且浏览器实现不太乐观。

这个 API 为开发者提供了一种可以异步监听目标元素与视窗 (viewport) 交叉状态的手段。 下面是一个懒加载模板内容的例子。

 
           
  1. function query(selector) {

  2.  return Array.from(document.querySelectorAll(selector));

  3. }

  4. var observer = new IntersectionObserver(

  5.  function(changes) {

  6.    changes.forEach(function(change) {

  7.      var container = change.target;

  8.      var content = container.querySelector('template').content;

  9.      container.appendChild(content);

  10.      observer.unobserve(container);

  11.    });

  12.  }

  13. );

  14. query('.lazy-loaded').forEach(function (item) {

  15.  observer.observe(item);

  16. });

这个 API 使懒加载变得简单了,由于不需要监听容器的滚动事件,全部由原生的观察者进行了异步回调。

四、扩展与优化

1、添加节流

scroll 事件,在滚动过程中,由于检测是否在视窗内的逻辑频繁被触发。因而频繁执行 DOM 操作、资源加载等浏览器负担消耗比较严重的行为。如果不加以控制很可能导致 UI 停顿甚至浏览器崩溃。

 
           
  1. // 添加节流控制。

  2. if (_setting.throttleMS > 0) {

  3.    this.update = throttle(this.update, _setting.throttleMS);

  4. }

2、添加仅垂直方向判断

通常懒加载情况 scroll 事件绑定在 window 对象,整个页面又是从上到下进行渲染,所以,横向的元素判断是否在视窗内通常没有意义。设置开关,可以减少横向判断逻辑,减少资源消耗。

 
           
  1. // 只判断垂直方向元素是否出现在视窗内

  2. if (setting.vertical) {

  3.    if(jqThis.height() <= 0 || jqThis.css('display') === 'none'){

  4.        return;

  5.    }

  6.    if ($.abovethetop(this, setting)) {

  7.        // Nothing.

  8.    } else if (!$.belowthefold(this, setting)) {

  9.        jqThis.trigger(APPEAR_EVENT);

  10.        counter = 0;

  11.    } else {

  12.        if (++counter > setting.failureLimit) {

  13.            return false;

  14.        }

  15.    }

  16. }

3、扩展功能

前面可能说过,懒加载并不局限与图片情况,有时根据业务需求可以扩展需要延迟加载的静态资源。比如:js 外链脚本、css 外链样式、视频、音频等。或者只放出回调,后续具体业务逻辑交由开发自行处理。 具体思路,可以在初始化时,配置指定参数决定当前懒加载实例执行哪种功能加载。也可以在站位 DOM 中配置属性进行懒加载类型判断,可以根据业务需要灵活处理。

五、总结

懒加载对于前端优化来讲,可以称得上是一种万金油的优化方式,很多场景都可以使用其提升页面体验。 但懒加载核心是判断元素是否在视窗内的操作,这种判断是要在视窗滚动过程中获取元素宽高,导致页面会频繁重绘,会耗费很大的性能开销。 而且在监听 DOM 方面,如果懒加载的逻辑是延迟渲染某段 HTML 代码的话,那么在绑定 DOM 事件方面也是有更多详细考虑才行,否则可能会出现监听事件失效的问题。 所以,在应用懒加载的场景,还是要多多考虑,如果页面资源较少,页面整体逻辑较简单,可以不使用懒加载,一次性加载完成体验可能还会更优于懒加载优化效果。 希望大家可以在工作中合理运用懒加载思想,做到页面的更好体验。

 
Qunar技术沙龙 更多文章 基于Flink构建用户实时基础行为工程 故宫“瘫痪”程序员怎么办? Node框架——Nomi.js Qunar技术沙龙开工啦!欢迎大家继续关注! Qunar技术沙龙给大家拜年啦!
猜您喜欢 携程无线离线包增量更新方案实践 binlog2sql 实现 MySQL 误操作的恢复 php字符串过滤器 .NET 开发环境搭建 我爱你