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

来源:互联网 发布:mac编辑hosts文件 编辑:程序博客网 时间:2024/06/07 00:17

背景

一个庞大的页面, 有时我们并不会滚动去看下面的内容, 这样就造成了非首屏部分的渲染, 这些无用的渲染不仅包括图片还包括其他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. 要让首屏尽快出来, 得给浏览器减轻渲染首屏的工作量. 可以从两方面入手:

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

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

优点

* 减少首屏DOM渲染,* 加快首屏加载速度* 分块加载js/css(使用于模块区分度高的网站)

缺点

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

SEO解决方案

关于美团BigRender技术的SEO解决方案:

如果放弃BigRender手段, 虽然可以提升SEO效果, 但也会因为网页打开变慢使用户体验受损.和技术权衡后尝试了一种解决方案, 将原有的大量团购单链接分别置于多个<textarea>内部.测试证明有效, 搜索引擎认为多个<textarea>构成的网页仍是信息丰富的, 排名有非常显著的提升.

BigRender 完整示例:

css:

ul {    width: 300px;    padding: 0;    list-style: none;}.lazy {    width: 300px;    height: 168px;    margin-bottom: 100px;    background: #0cf;}.datalazyload {    width: 300px;    height: 168px;}

html:

<ul>    <div class="">        <li class="lazy"></li>    </div>    <div class="">        <li class="lazy"></li>    </div>    <div class="">        <li class="lazy"></li>    </div>    <div class="loading">        <textarea class="datalazyload" style="visibility: hidden;">            <style>.lazy {width: 300px; height: 168px; background: #333;}</style>            <script>console.log('eval success');</script>            <li class='lazy'></li>        </textarea>    </div>    <div class="loading">        <textarea class="datalazyload" style="visibility: hidden;">            <style>.lazy {width: 300px; height: 168px; background: #444;}</style>            <script>console.log('eval success');</script>            <li class='lazy'></li>        </textarea>    </div>    <div class="loading">        <textarea class="datalazyload" style="visibility: hidden;">            <style>.lazy {width: 300px; height: 168px; background: #555;}</style>            <script>console.log('eval success');</script>            <li class='lazy'></li>        </textarea>    </div>    <div class="loading">        <textarea class="datalazyload" style="visibility: hidden;">            <style>.lazy {width: 300px; height: 168px; background: #666;}</style>            <script>console.log('eval success');</script>            <li class='lazy'></li>        </textarea>    </div>    <div class="loading">        <textarea class="datalazyload" style="visibility: hidden;">            <style>.lazy {width: 300px; height: 168px; background: #777;}</style>            <script>console.log('eval success');</script>            <li class='lazy'></li>        </textarea>    </div>    <div class="loading">        <textarea class="datalazyload" style="visibility: hidden;">            <style>.lazy {width: 300px; height: 168px; background: #888;}</style>            <script>console.log('eval success');</script>            <li class='lazy'></li>        </textarea>    </div>    <div class="loading">        <textarea class="datalazyload" style="visibility: hidden;">            <style>.lazy {width: 300px; height: 168px; background: #999;}</style>            <script>console.log('eval success');</script>            <li class='lazy'></li>        </textarea>    </div>    <div class="loading">        <textarea class="datalazyload" style="visibility: hidden;">            <style>.lazy {width: 300px; height: 168px; background: #ccc;}</style>            <script>console.log('eval success');</script>            <li class='lazy'></li>        </textarea>    </div>    <div class="loading">        <textarea class="datalazyload" style="visibility: hidden;">            <style>.lazy {width: 300px; height: 168px; background: #bbb;}</style>            <script>console.log('eval success');</script>            <li class='lazy'></li>        </textarea>    </div>    <div class="loading">        <textarea class="datalazyload" style="visibility: hidden;">            <style>.lazy {width: 300px; height: 168px; background: #aaa;}</style>            <script>console.log('eval success');</script>            <li class='lazy'></li>        </textarea>    </div></ul>

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 + "[^>]*>([\\S\\s]*?)</" + cata + "\\s*>",                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, ">").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("<script[^>]*>([\\S\\s]*?)</script\\s*>", "img"), "")                .replace(new RegExp("<style[^>]*>([\\S\\s]*?)</style\\s*>", "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          // 距离底部多高,进行延迟加载的阈值});

参考原文

0 0
原创粉丝点击