资讯专栏INFORMATION COLUMN

关于性能优化的那点事——BigRender首屏渲染优化

Markxu / 3297人阅读

摘要:首屏渲染优化背景一个庞大的页面有时我们并不会滚动去看下面的内容这样就造成了非首屏部分的渲染这些无用的渲染不仅包括图片还包括其他元素甚至一些某些根据模块请求比如理论上每增加一个都会增加渲染的时间并且影响着页面打开的加载速度这时就需要一种办法使

BigRender首屏渲染优化 背景

一个庞大的页面, 有时我们并不会滚动去看下面的内容, 这样就造成了非首屏部分的渲染, 这些无用的渲染不仅包括图片还包括其他DOM元素, 甚至一些js/css(某些js/css根据模块请求,比如ajax), 理论上每增加一个DOM, 都会增加渲染的时间, 并且影响着页面打开的加载速度.这时就需要一种办法使得html, js, css实现按需加载.

案例

新浪, 美团, 途牛旅行网, 360网址导航, 淘宝商品详情页等等.查看它们的源代码(ctrl+u), ctrl+f 搜索 textarea 关键字, 很容易可以看到一些被textarea标签包裹的HTML代码.

原理

使用textarea标签包裹HTML/JS/CSS代码, 当作textarea的value值, 在页面渲染的时候实际并没有渲染到DOM树上, 而是与图片懒加载类似, 当textarea标签出现或即将出现在用户视野时, 将textarea中的HTML代码取出, 用innerHTML动态插入到DOM树中, 如有必要使用正则取出js/css代码动态执行.

玉伯指出:
页面下载完毕后, 要经过Tokenization - Tree Construction - Rendering. 要让首屏尽快出来, 得给浏览器减轻渲染首屏的工作量. 可以从两方面入手:

减少DOM节点数, 节点数越少, 意味着Tokenization, Rendering等操作耗费的时间越少.(对于典型的淘宝商品详情页,经测试发现, 每增加一个DOM节点, 会导致首屏渲染时间延迟约0.5ms)

减少脚本执行时间. 脚本执行和UI Update共享一个thread, 脚本耗的时间约少, UI Update就能越发提前.

优点
* 减少首屏DOM渲染,
* 加快首屏加载速度
* 分块加载js/css(使用于模块区分度高的网站)
缺点
* 需要更改DOM结构
* 可能引起一些重排和重绘
* 没有开启js功能的用户将看不到延迟加载的内容
* 额外性能损耗(渲染前的textarea里面的html代码,在服务端把html代码保存在隐藏的textarea里面
  所以在服务端会把html代码转义, 尖括号等都被转义了, 会增加服务端的压力, 而且这个改造只是前端
  的渲染, 服务器依旧是一次计算所有的数据, 输出所有的数据. 一般使用都是由后端拼接成html字符串
  然后塞入textarea标签, 吐给前端)
* 不利于SEO(在搜索引擎看来网页也缺少了关键的DOM节点, 原本信息量丰富的网页内容被放入单个的
  
    
js:
;(function(win, doc) {

    // 兼容低版本 IE
    Function.prototype.bind = Function.prototype.bind || function(context) {
        var that = this;
        return function() {
            return that.apply(context, arguments);
        };
    };

    // 工具方法 begin
    var Util = {
        getElementsByClassName: function(cls) {
            if (doc.getElementsByClassName) {
                return doc.getElementsByClassName(cls);
            }

            var o = doc.getElementsByTagName("*"),
                rs = [];

            for (var i = 0, t, len = o.length; i < len; i++) {
                (t = o[i]) && ~t.className.indexOf(cls) && rs.push(t);
            }

            return rs;
        },
        addEvent: function(ele, type, fn) {
            ele.attachEvent ? ele.attachEvent("on" + type, fn) : ele.addEventListener(type, fn, false);
        },
        removeEvent: function(ele, type, fn) {
            ele.detachEvent ? ele.detachEvent("on" + type, fn) : ele.removeEventListener(type, fn, false);
        },
        getPos: function(ele) {
            var pos = {
                x: 0,
                y: 0
            };

            while (ele.offsetParent) {
                pos.x += ele.offsetLeft;
                pos.y += ele.offsetTop;
                ele = ele.offsetParent;
            }

            return pos;
        },
        getViewport: function() {
            var html = doc.documentElement;

            return {
                w: !window.innerWidth ? html.clientHeight : window.innerWidth,
                h: !window.innerHeight ? html.clientHeight : window.innerHeight
            };
        },
        getScrollHeight: function() {
            html = doc.documentElement, bd = doc.body;
            return Math.max(window.pageYOffset || 0, html.scrollTop, bd.scrollTop);
        },
        getEleSize: function(ele) {
            return {
                w: ele.offsetWidth,
                h: ele.offsetHeight
            };
        }
    };
    // 工具方法 end

    var Datalazyload = {
        threshold: 0,  // {number} 阈值,预加载高度,单位(px)
        els: null,  // {Array} 延迟加载元素集合(数组)
        fn: null,   // {Function} scroll、resize、touchmove 所绑定方法,即为 pollTextareas()

        evalScripts: function(code) {
            var head = doc.getElementsByTagName("head")[0],
                js = doc.createElement("script");

            js.text = code;
            head.insertBefore(js, head.firstChild);
            head.removeChild(js);
        },

        evalStyles: function(code) {
            var head = doc.getElementsByTagName("head")[0],
                css = doc.createElement("style");

            css.type = "text/css";

            try {
                css.appendChild(doc.createTextNode(code));
            } catch (e) {
                css.styleSheet.cssText = code;
            }

            head.appendChild(css);
        },

        extractCode: function(str, isStyle) {
            var cata = isStyle ? "style" : "script",
                scriptFragment = "<" + cata + "[^>]*>([Ss]*?)",
                matchAll = new RegExp(scriptFragment, "img"),
                matchOne = new RegExp(scriptFragment, "im"),
                matchResults = str.match(matchAll) || [],
                ret = [];

            for (var i = 0, len = matchResults.length; i < len; i++) {
                var temp = (matchResults[i].match(matchOne) || [ "", "" ])[1];
                temp && ret.push(temp);
            }
            return ret;
        },

        decodeHTML: function(str) {
            return str.replace(//g, ">").replace(/&/g, "&");
        },

        insert: function(ele) {
            var parent = ele.parentNode,
                txt = this.decodeHTML(ele.innerHTML),
                matchStyles = this.extractCode(txt, true),
                matchScripts = this.extractCode(txt);
            // console.log(txt)
            console.log(matchStyles);
            console.log(matchScripts);

            parent.innerHTML = txt
                .replace(new RegExp("]*>([Ss]*?)", "img"), "")
                .replace(new RegExp("]*>([Ss]*?)", "img"), "");

            if (matchStyles.length) {
                for (var i = matchStyles.length; i--;) {
                    this.evalStyles(matchStyles[i]);
                }
            }


            // 如果延迟部分需要做 loading 效果
            parent.className = parent.className.replace("loading", "");

            if (matchScripts.length) {
                for (var i = 0, len = matchScripts.length; i < len; i++) {
                    this.evalScripts(matchScripts[i]);
                }
            }
        },

        inView: function(ele) {
          var top = Util.getPos(ele).y
            , viewVal = Util.getViewport().h
            , scrollVal = Util.getScrollHeight()
            , eleHeight = Util.getEleSize(ele).h;

          if (top >= scrollVal  - eleHeight - this.threshold && top <= scrollVal + viewVal + this.threshold) {
            return true;
          }

          return false;
        },

        pollTextareas: function() {
            // 需延迟加载的元素已经全部加载完
            if (!this.els.length) {
                Util.removeEvent(window, "scroll", this.fn);
                Util.removeEvent(window, "resize", this.fn);
                Util.removeEvent(doc.body, "touchMove", this.fn);
                return;
            }

            // 判断是否需要加载
            for (var i = this.els.length; i--; ) {
                var ele = this.els[i];

                if (!this.inView(ele)) {
                    continue;
                }

                this.insert(ele);
                this.els.splice(i, 1);
            }
        },

        init: function(config) {
            var cls = config.cls;
            this.threshold = config.threshold ? config.threshold : 0;

            this.els = Array.prototype.slice.call(Util.getElementsByClassName(cls));
            this.fn = this.pollTextareas.bind(this);

            this.fn();
            Util.addEvent(window, "scroll", this.fn);
            Util.addEvent(window, "resize", this.fn);
            Util.addEvent(doc.body, "touchMove", this.fn);
        }
    };

    win["datalazyload"] = Datalazyload;
})(window, document);


// demo:
datalazyload.init({
    cls: "datalazyload",    // 需要延迟加载的类,即 textarea 的类名
    threshold: 100          // 距离底部多高,进行延迟加载的阈值
});

参考原文

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/80216.html

相关文章

  • 关于性能优化的那点事——函数节流

    摘要:函数节流背景中的函数大多数情况下都是由用户主动调用触发的除非是函数本身的实现不合理否则一般不会遇到跟性能相关的问题但在少数情况下函数的触发不是由用户直接控制的在这些场景下函数可能被非常频繁调用而造成大的性能问题场景事件事件滚动事件共同的特征 函数节流 背景 javascript中的函数大多数情况下都是由用户主动调用触发的, 除非是函数本身的实现不合理, 否则一般不会遇到跟性能相关的问题...

    khlbat 评论0 收藏0
  • 关于性能优化的那点事——函数防抖

    摘要:函数防抖场景假设网站有个搜索框用户输入文本我们会自动联想匹配出一些结果供用户选择我们可能首先想到的做法就是监听事件然后异步查询结果但是如果用户快速的输入了一串字符假设是个字符那么就会在瞬间触发次请求这无疑不是我们想要的我们想要的是用户停止输 函数防抖 场景 假设网站有个搜索框, 用户输入文本我们会自动联想匹配出一些结果供用户选择,我们可能首先想到的做法就是监听keypress事件, 然...

    Stardustsky 评论0 收藏0
  • 查漏补缺 - 收藏集 - 掘金

    摘要:酝酿许久之后,笔者准备接下来撰写前端面试题系列文章,内容涵盖浏览器框架分钟搞定常用基础知识前端掘金基础智商划重点在实际开发中,已经非常普及了。 这道题--致敬各位10年阿里的前端开发 - 掘金很巧合,我在认识了两位同是10年工作经验的阿里前端开发小伙伴,不但要向前辈学习,我有时候还会选择另一种方法逗逗他们,拿了网上一道经典面试题,可能我连去阿里面试的机会都没有,但是我感受到了一次面试1...

    YuboonaZhang 评论0 收藏0
  • 关于localStorage面试的那点事

    摘要:已经超出本地存储限定大小可进行超出限定大小之后的操作,如下面可以先清除记录,再次保存面试官一波素质三连对于只是会使用的同学来说,肯定是不得其解的。 最近面试的时候关于html5API总会被问到localStorage的问题, 对于一般的问题很简单,无非就是 localStorage、sessionStorage和cookie这三个客户端缓存的区别 localStorage的API,g...

    timger 评论0 收藏0
  • 关于var,let,const的那点事

    摘要:一直使用定义变量,的出现给变量定义增加了两个大将,。声明的变量,块作用域,不重复声明覆盖,限制了变量的作用域,保证变量不会去污染全局变量,所以尽量将改为用。 一直使用var定义变量,ES6的出现给变量定义增加了两个大将let,const。那它们有什么区别呢。 1、const关键字它的作用就是定义一个常量,一旦定义无法更改,不能重复声明覆盖; showImg(https://segmen...

    KavenFan 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<