摘要:首屏渲染优化背景一个庞大的页面有时我们并不会滚动去看下面的内容这样就造成了非首屏部分的渲染这些无用的渲染不仅包括图片还包括其他元素甚至一些某些根据模块请求比如理论上每增加一个都会增加渲染的时间并且影响着页面打开的加载速度这时就需要一种办法使
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节点, 原本信息量丰富的网页内容被放入单个的SEO解决方案
关于美团BigRender技术的SEO解决方案:
如果放弃BigRender手段, 虽然可以提升SEO效果, 但也会因为网页打开变慢使用户体验受损.和技术权衡后尝试了一种解决方案, 将原有的大量团购单链接分别置于多个
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:
;(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]*?)" + 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, "&"); }, 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("