關於效能優化的那點事——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節點, 原本資訊量豐富的網頁內容被放入單個的
<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 // 距離底部多高,進行延遲載入的閾值
});