当一个网站越来越庞大,加载速度越来越慢的时候,开发者们不得不对其进行优化,谁愿意访问一个需要等待 10 秒,20 秒才能出现的网页呢?

常见的也是相对简单易行的一个优化方案是 图片的延迟加载。一个庞大的页面,有时我们并不会滚动去看下面的内容,这样就浪费了非首屏部分的渲染,而这些无用的渲染,不仅包括图片,还包括其他的 DOM 元素,甚至一些 js/css(某些js/css 是根据模块请求的,比如一些 ajax),理论上,每增加一个 DOM,都会增加渲染的时间。有没有办法能使得 HTML、js、css 都能按需加载呢?答案是肯定的,这就是本文要讲的 BigRender。

业界有很多 BigRender 在生产环境中的案例,比如 新浪美团途牛旅行网360网址导航淘宝商品详情页 等等。查看它们的源代码(ctrl+u),ctrl+f 搜索 textarea 关键字,很容易可以看到一些被 textarea 标签包裹的 HTML 代码。

比如途牛:

而这些被 textarea 标签包裹的 HTML 代码,只是 textarea 的 value 值,并没有渲染到 DOM 树上。没错,BigRender 通常就是用 textarea 标签包裹 HTML 代码(js/css),当作其 value 值,等到合适的时机(通常当 textarea 标签出现或者即将出现在用户视野时)将 textarea 中的 HTML 代码取出,用 innerHTML 动态插入到 DOM 树中,如有必要,取出 js/css 代码(正则),动态执行它们。(是不是和图片的延迟加载很相似?)

玉伯指出

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

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

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

为什么是用 textarea 标签存放大块 HTML 内容?还是可以看下玉伯的 这篇文章。淘宝的 kissy 就内置了 DataLazyload 组件。(插播:美团详情页还有用到 script 标签做 BigRender 优化,详情请见下面的 "其他" 一节)

接下去就来一步一步实现一个适合自己的 BigRender 插件,我希望可以延迟加载 HTML 元素、js 以及 css。

T.datalazyload

仿照 jQuery 的写法我定义了一个全局对象 T,将延迟加载的实现代码封装在了 T.datalazyload 对象中,将需要延迟加载的代码 "包裹" 在 textarea 标签中,设置其 visibility 属性为 hidden,并赋予该标签一个特殊的类名(为了做事件监听),比如叫做 "datalazyload"。为了方便,我规定每个做 bigrender 优化的 textarea 的父节点都只有一个子孩子(即该 textarea 元素),这一点非常重要必须遵守,因为后面代码有针对此的特殊处理。(注意要设置好父节点的高度宽度,和 dom 渲染后的高度宽度保持一致)

一些 HTML/js/css 代码都可以包裹在 textarea 标签中,例如:

<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>

init

给 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() 方法。

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

接下去看 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]);
},

extractCode

我们通过正则将 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/evalStyles

脚本执行,样式渲染。

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 标签中,吐给前端。

demo

如果要做一个完整的 BigRender demo,可能比较复杂,还要涉及到后端。

之前学习 lazyload 时做过一个图片的延迟加载 demo,seehttp://hanzichi.github.io/cnblogs/2016/Feb/picture-lazyload/。因为 BigRender 是 lazyload 的加强版,所以简单地做了个 BigRender 版本的图片延迟加载http://hanzichi.github.io/cnblogs/2016/Mar/bigrender/,实现的具体代码可以 checkbigrender.js。求 star,求 fork~

其他

除了首页部分用了 textarea 做 BigRender 优化外,美团还用到了 script 标签做优化。比如 这个商品详情页

给 script 标签设置个非 "text/javascript" 的 type,可以下载这段 js,但不执行,这种做法似曾相识,在 labjs 中看到过。

更多可以参考 前端优化三续:用script存放html代码来减少DOM节点数

Read More

BigRender的更多相关文章

  1. 大流量网站性能优化:一步一步打造一个适合自己的BigRender插件

    BigRender 当一个网站越来越庞大,加载速度越来越慢的时候,开发者们不得不对其进行优化,谁愿意访问一个需要等待 10 秒,20 秒才能出现的网页呢? 常见的也是相对简单易行的一个优化方案是 图片 ...

  2. 前端优化:BigRender

    前言 有对象才叫跨年,没对象叫熬夜.所以,在这没对象的元旦假期的夜里捣弄了一下前端优化的东西.如果你听说过FaceBook,太好了,你肯定是个网 络潮人:如果你还听说过FaceBook的bigpipe ...

  3. bigpipe&bigrender

    bigpipe: 先输出页面的整体布局,在按块输出输出页面的每个部分.这样可以让服务器的运算.网络的传输和浏览器的渲染并行.适用于服务器运算较慢的时候. bigrender: 主要在浏览器端,先将字符 ...

  4. 2016 daily

    2016.01.06 leetcode 切题数达到 200+,截止目前 137.虽然一年 63 题看似不多,但是 easy 的题目基本已经切完,质量 >> 数量(专注 leetcode,可 ...

  5. 关于 bind 你可能需要了解的知识点以及使用场景

    不看不知道,一看吓一跳,已经整整一个月没有更新 underscore 源码解读系列文章了.前面我们已经完成了 Object ,Array,Collection 上的扩展方法的源码剖析,本文开始来解读 ...

  6. 让 innerHTML 进来的 script 代码跑起来

    今天来简单聊聊如何让 innerHTML 进来的 scrip 代码跑起来的问题. 前台请求一个接口,接口返回一些 HTML 标签拼接成的字符串,以供前端直接 innerHTML 生成 DOM 元素,这 ...

  7. -_-#【减少 DOM 元素】textarea, script 延迟渲染

    淘宝详情页的 BigRender 优化与存放大块 HTML 内容的最佳方式 淘宝详情页的BigRender优化的最佳方式 <!DOCTYPE html> <html> < ...

  8. 阿里巴巴 web前端性能优化进阶路

    Web前端性能优化WPO,相信大多数前端同学都不会陌生,在各自所负责的站点页面中,也都会或多或少的有过一定的技术实践.可以说,这个领域并不缺乏成熟技术理论和技术牛人:例如Yahoo的web站点性能优化 ...

  9. nodeJS实现简单网页爬虫功能

    前面的话 本文将使用nodeJS实现一个简单的网页爬虫功能 网页源码 使用http.get()方法获取网页源码,以hao123网站的头条页面为例 http://tuijian.hao123.com/h ...

随机推荐

  1. 【iOS】init,loadView,viewDidLoad加载关系

    一.loadView 永远不要主动调用这个函数.view controller会在view的property被请求并且当前view值为nil时调用这个函数.如果你手动创建view,你应该重载这个函数. ...

  2. dnf脚本的研究

    [player number] 2 8  [pvp start area]0 0 0 00 0 0 00 0 0 0[dungeon]62[/dungeon][type] `[normal]`[gre ...

  3. MyEcpilise引入Maven项目目录不正常,无JRE,无Maven Dependencies

    右键项目--> Maven4MyEclipse --> Update Project Configuration

  4. unity脚本的基础语法

    基本的回调方法 Strat()方法:在游戏场景加载时被调用,在该方法内可以写一些游戏场景初始化之类的代码. update():在每一帧渲染之前被调用,大部分游戏代码在这里执行,除了物理部分的代码. F ...

  5. MapReduce工作原理图文详解 (炼数成金)

    MapReduce工作原理图文详解 1.Map-Reduce 工作机制剖析图: 1.首先,第一步,我们先编写好我们的map-reduce程序,然后在一个client 节点里面进行提交.(一般来说可以在 ...

  6. ppshu

    全部书籍已经下载完毕! http://3cvpkfx4gdnkcduj.onion/ https://3cvpkfx4gdnkcduj.onion.cab/ https://3cvpkfx4gdnkc ...

  7. Unity寻路的功能总结

    源地址:http://blog.csdn.net/sgnyyy/article/details/21878163 1. 利用Unity本身自带的NavMesh 这篇文章已经比较详细,可能对于很多需要a ...

  8. Python的作用域

    Python的作用域 转自:http://www.cnblogs.com/frydsh/archive/2012/08/12/2602100.html Python是静态作用域语言,尽管它自身是一个动 ...

  9. C/C++中几种经典的垃圾回收算法

    1.引用计数算法 引用计数(Reference Counting)算法是每个对象计算指向它的指针的数量,当有一个指针指向自己时计数值加1:当删除一个指向自己的指针时,计数值减1,如果计数值减为0,说明 ...

  10. eclipse查看jar包中class的中文注释乱码问题的解决

    1,问题来源是在eclipse中直接查看springside的class(由eclipse自动反编译)里面注释的乱码问题: Preferences-General-Workspace-Text fil ...