当一个网站愈来愈庞大,加载速度愈来愈慢的时候,开发者们不得不对其进行优化,谁愿意访问一个须要等待 10 秒,20 秒才能出现的网页呢?javascript
常见的也是相对简单易行的一个优化方案是 图片的延迟加载。一个庞大的页面,有时咱们并不会滚动去看下面的内容,这样就浪费了非首屏部分的渲染,而这些无用的渲染,不只包括图片,还包括其余的 DOM 元素,甚至一些 js/css(某些js/css 是根据模块请求的,好比一些 ajax),理论上,每增长一个 DOM,都会增长渲染的时间。有没有办法能使得 HTML、js、css 都能按需加载呢?答案是确定的,这就是本文要讲的 BigRender。php
业界有不少 BigRender 在生产环境中的案例,好比 新浪,美团,途牛旅行网,360网址导航,淘宝商品详情页 等等。查看它们的源代码(ctrl+u),ctrl+f 搜索 textarea 关键字,很容易能够看到一些被 textarea 标签包裹的 HTML 代码。css
好比途牛:html
而这些被 textarea 标签包裹的 HTML 代码,只是 textarea 的 value 值,并无渲染到 DOM 树上。没错,BigRender 一般就是用 textarea 标签包裹 HTML 代码(js/css),看成其 value 值,等到合适的时机(一般当 textarea 标签出现或者即将出如今用户视野时)将 textarea 中的 HTML 代码取出,用 innerHTML 动态插入到 DOM 树中,若有必要,取出 js/css 代码(正则),动态执行它们。(是否是和图片的延迟加载很类似?)前端
玉伯指出:java
页面下载完毕后,要通过 Tokenization — Tree Construction — Rendering. 要让首屏尽快出来,得给浏览器减轻渲染首屏的工做量。能够从两方面入手:
1. 减小 DOM 节点数。节点数越少,意味着 Tokenization, Rendering 等操做耗费的时间越少。(对于典型的淘宝商品详情页,经测试发现,每增长一个 DOM 节点,会致使首屏渲染时间延迟约 0.5ms.)
2. 减小脚本执行时间。脚本执行和 UI Update 共享一个 thread, 脚本耗的时间越少,UI Update 就能愈加提早。
为何是用 textarea 标签存放大块 HTML 内容?仍是能够看下玉伯的 这篇文章。淘宝的 kissy 就内置了 DataLazyload 组件。(插播:美团详情页还有用到 script 标签作 BigRender 优化,详情请见下面的 "其余" 一节)git
接下去就来一步一步实现一个适合本身的 BigRender 插件,我但愿能够延迟加载 HTML 元素、js 以及 css。github
仿照 jQuery 的写法我定义了一个全局对象 T,将延迟加载的实现代码封装在了 T.datalazyload 对象中,将须要延迟加载的代码 "包裹" 在 textarea 标签中,设置其 visibility 属性为 hidden,并赋予该标签一个特殊的类名(为了作事件监听),好比叫作 "datalazyload"。为了方便,我规定每一个作 bigrender 优化的 textarea 的父节点都只有一个子孩子(即该 textarea 元素),这一点很是重要必须遵照,由于后面代码有针对此的特殊处理。(注意要设置好父节点的高度宽度,和 dom 渲染后的高度宽度保持一致)ajax
一些 HTML/js/css 代码均可以包裹在 textarea 标签中,例如:sql
<textarea class="datalazyload" style="visibility: hidden;"> <script type="text/javascript"> alert("I am lazyload zone!"); </script> <style type="text/css"> .main {margin: 0 auto; text-align: center; padding-top: 200px; width:1000px; height:1000px; border:5px black dashed;} .second {margin: 0 auto; width:1000px; height:200px; border: 5px purple dotted; padding-top: 100px; text-align: center;} </style> <div class="second"> <h1>我是延迟加载的部分!</h1> </div> </textarea>
给 T.datalazyload 对象定义一个 init() 方法,初始化页面时监听 scroll、resize 以及移动端的 touchmove 事件,当触发这些事件时,回调函数内判断延迟加载部分是否已经出如今视口。
init: function(config) { var cls = config.cls; this.threshold = config.threshold ? config.threshold : 0; this.els = Array.prototype.slice.call(T.getElementsByClassName(cls)); this.fn = this.pollTextareas.bind(this); this.fn(); T.addEvent(window, "scroll", this.fn); T.addEvent(window, "resize", this.fn); T.addEvent(doc.body, "touchMove", this.fn); }
config 是配置参数,其 cls 属性表示须要延迟加载的 textarea 的类名,threshold 为阈值,单位 px,表示当 textarea 距离视口多少像素时,进行预加载。
将须要延迟加载的元素存入一个数组(this.els),(某 textarea 元素)后续一旦完成加载随即在数组中删除该元素。事件监听的回调函数为 pollTextarea() 方法。
pollTextareas: function() { // 需延迟加载的元素已经所有加载完 if (!this.els.length) { T.removeEvent(window, "scroll", this.fn); T.removeEvent(window, "resize", this.fn); T.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); } }
这个方法的做用是判断须要延迟加载的元素是否已经在视口,若是是,则进行加载(触发 insert 方法),而且在数组中删除该元素;若是数组为空,则代表须要延迟加载的部分都已经加载完,移除事件监听,整个延迟加载结束。
接下去看 insert 方法。inert 方法的参数是须要延迟加载的 textarea 元素,很显然,咱们须要解析的代码全在 textarea.innerHTML 中。咱们用 extractCode 方法取出其中的 js/css 代码,而后将 js/css 过滤掉,这样剩下的就全是 HTML 代码了,将其插入 DOM 中(这正是前文说的 "每一个 textarea 的父节点都只有一个子孩子" 的缘由,能够直接用父节点 innerHTML 操做),若是有 loading 效果,通常在父节点加个 loading 类,移除便可。最后再动态执行 js 脚本,插入 css 样式。
insert: function(ele) { var parent = ele.parentNode , txt = this.decodeHTML(ele.innerHTML) , matchStyles = this.extractCode(txt, true) , matchScripts = this.extractCode(txt); 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]); },
咱们经过正则将 js 和 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; }
成功地将 script 以及 style 标签内的内容提取了出来,巧妙地用了正则中的子表达式。
脚本执行,样式渲染。
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); }
简单讲讲 BigRender 优化的优缺点,以及适用场景。
优势很明显,由于减小了首屏 DOM 的渲染,因此能加快首屏加载的速度,而且能分块加载 js/css,很是适用于一些模块区分度很高的网站(我的以为大型网站的模块区分度广泛愈来愈高了)。
缺点是须要更改 DOM 结构(DOM 节点的替换和渲染),可能会引发一些重排和重绘。一些没有开启 js 功能的用户将看不到延迟加载的内容(能够用 noscript 标签给出一个善意提醒)。最大的缺点多是不利于 SEO,一些依赖于 SEO 的网站可能须要在 SEO 上下点功夫了,好比美团。
关于 SEO,能够看下 http://www.seoqx.com/lynx 这个网站,能模拟搜索引擎蜘蛛对网站的爬取状况。美团对于 BigRender 以及 SEO 解决方案 [美团网案例]改善BigRender技术致使的SEO问题
bigrender 经过减小 DOM 节点,加快首屏的渲染,可是,它也是有额外的性能损耗的,渲染前textarea 里面的 html 代码,在服务端把 html 代码保存在隐藏的 textarea 里面,因此在服务端会把 html 代码转义:尖括号等都被转义了,这个会增长服务器的压力;并且,这个改造只是前端的渲染,服务器依旧是一次计算全部的数据,输出全部的数据,这一点没有获得提升。
通常来讲,使用都是后端拼接成 html 字符串,而后塞入 textarea 标签中,吐给前端。
若是要作一个完整的 BigRender demo,可能比较复杂,还要涉及到后端。
以前学习 lazyload 时作过一个图片的延迟加载 demo,see http://hanzichi.github.io/cnblogs/2016/Feb/picture-lazyload/。由于 BigRender 是 lazyload 的增强版,因此简单地作了个 BigRender 版本的图片延迟加载 http://hanzichi.github.io/cnblogs/2016/Mar/bigrender/,实现的具体代码能够 check bigrender.js。求 star,求 fork~
除了首页部分用了 textarea 作 BigRender 优化外,美团还用到了 script 标签作优化。好比 这个商品详情页
给 script 标签设置个非 "text/javascript" 的 type,能够下载这段 js,但不执行,这种作法似曾相识,在 labjs 中看到过。
更多能够参考 前端优化三续:用script存放html代码来减小DOM节点数
http://www.cnblogs.com/zichi/p/5252629.html