前端優化常用技術
前端優化常用技術
從建立 http 連線開始,到頁面展示在瀏覽器中,一共經過了載入,執行,渲染,重構等幾個階段。
載入
PV(訪問量):即 Page View,頁面重新整理一次算一次。也叫併發量。
UV(獨立訪客):即 Unique Visitor,一個客戶端(電腦,手機)為一個訪客
比如:百萬級的 PV,併發量過大怎麼辦 ?
由此,前端所有的優化都是基於這個點和單執行緒而延伸出來的。所以,前端的資源載入優化有兩個方向:
- 開源
增加域名,簡單來說就是 cdn。
- 節流
資源壓縮,去除空格,gzip 等。
一旦開發中引入的 UI 庫或第三方外掛多了,總檔案體量也不在少數;就有了:按需載入、延時載入的用武之地。
比如在 webpack 打包的時候從 template 的 html 中單獨加入某個 css 或 js;
另外,圖片也需要做很多相應的處理:
- css 實現效果(按鈕、陰影等)
- 壓縮尺寸和 size
- sprite 合併
- svg、toff 字型圖
- canvas 繪製大圖(地圖相關)
阻塞性優化
js 頁面載入之後是否要立即執行?立即執行是否會影響頁面渲染?
過去瀏覽器在載入和執行 js 檔案時是阻塞狀態,就是按照棧原理一個一個來;所以,原來要求把 js 檔案放到 html 程式碼底部前。
現代瀏覽器某種程度上解決了並行載入的問題,也可以進行預載入,但是執行之後會否對頁面造成重排?
所以要靈活應用 dns-prefetch、preload 和 defer|async。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Demo</title>
<link rel="dns-prefetch" href="//cdn.com/">
<link rel="preload" href="//js.cdn.com/currentPage-part1.js" as="script">
<link rel="preload" href="//js.cdn.com/currentPage-part2.js" as="script">
<link rel="preload" href="//js.cdn.com/currentPage-part3.js" as="script">
<link rel="prefetch" href="//js.cdn.com/prefetch.js">
</head>
<body>
<!-- html code -->
<script type="text/javascript" src="//js.cdn.com/currentPage-part1.js" defer></script>
<script type="text/javascript" src="//js.cdn.com/currentPage-part2.js" defer></script>
<script type="text/javascript" src="//js.cdn.com/currentPage-part3.js" defer></script>
</body>
</html>
## 執行優化
作用域優化
- 變數層級不要巢狀太深。
(function(w, d) {})(window, document);
// 目的就是如此,再比如說的快取某個變數或物件
function check() {
var d = document,
t = document.getElementById("t"),
l = t.children;
for (let i = 0; i < l; i++) {
//code
}
}
迴圈優化
- 簡化終止條件
for (var i = 0; i < values.length; i++) {
process(values[i]);
}
for (var i = 0, len = values.length; i < len; i++) {
process(values[i]);
}
- 展開迴圈
// 針對大資料集使用展開迴圈可以節省很多時間,但對於小資料集,額外的開銷則可能得不償失。
function process(v) {
alert(v);
}
var values = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17];
var iterations = Math.floor(values.length / 8); // 商
var leftover = values.length % 8; // 餘數
var i = 0;
// 一共迴圈的次數 = 餘數的次數 + 商*除數(8)的次數
if (leftover > 0) {
do {
process(values[i++]);
} while (--leftover > 0);
}
do {
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
process(values[i++]);
} while (--iterations > 0);
避免雙重解釋
eval("console.log('hello world');"); // 避免
var sayHi = new Function("console.log('hello world');"); // 避免
setTimeout("console.log('hello world');", 100); // 避免
/**
* 以上程式碼是包含在字串中的,即在JS程式碼執行的同時必須新啟運一個解析器來解析新的程式碼。
* 例項化一個新的解析器有不容忽視的開銷,故這種程式碼要比直接解析要慢。
* 正確的應該這麼做:
*/
console.log("hello world");
var sayHi = function() {
console.log("hello world");
};
setTimeout(function() {
console.log("hello world");
}, 100);
最小化語句數
- 多個變數宣告
// 避免
var i = 1;
var j = "hello";
var arr = [1, 2, 3];
var now = new Date();
// 提倡
var i = 1,
j = "hello",
arr = [1, 2, 3],
now = new Date();
- 插入迭代值
// 避免
var name = values[i];
i++;
// 提倡
var name = values[i++];
- 使用陣列和物件字面量,避免使用建構函式 Array(),Object()
// 避免
var a = new Array();
a[0] = 1;
a[1] = "hello";
a[2] = 45;
var o = new Obejct();
o.name = "bill";
o.age = 13;
// 提倡
var a = [1, "hello", 45];
var o = {
name: "bill",
age: 13
};
效能的其它注意事項
- 原生方法更快
用諸如 C/C++之類的編譯型語言寫出來的,要比 JS 的快多了。
- switch 語句較快
如果有一系列的複雜的 if-else 語句,可以轉換成單個的 switch 語句會更快,還可以通過 case 語句按照最可能的到最不可能的順序進行組織,進一步優化程式碼。
資料儲存
通過改變資料儲存的位置來獲取最佳的讀寫效能。
- 字面量 字面量就代表自身,不儲存在特定位置。JS 字面量有:字串、數字、布林、物件、陣列、函式、正則表示式和特殊的 null、undefined
- 本地變數 使用 var 定義的資料儲存單元
- 陣列物件 儲存在 JS 物件內部
- 物件成員 儲存在 JS 物件內部
儘量使用字面量和區域性變數,減少陣列項和物件成員的使用。
作用域
生效的範圍(域),哪些變數可以被函式訪問,this 的賦值,上下文(context)的轉換。
function fn(a, b) {
return (res = a * b);
}
當 fn 被建立時,它的作用域鏈(內部屬性[[Scope]])中插入了一個物件變數, 這個全域性物件代表著在全域性範圍內定義的所有變數。
全域性物件包括:window、navigator、document 等。
fn 執行的時候就會用到作用域,並建立執行環境也叫執行上下文。它定義了一個函式執行時的環境,即便是同一個函式,每次執行都建立新的環境,函式執行完畢,環境就銷燬。
每個環境都要根據作用域和作用域鏈解析引數,變數。
而閉包的是根據 JS 允許函式訪問區域性作用域之外的資料,雖然會帶來效能問題,因為執行環境雖然銷燬,但啟用的物件依然存在,所以可以快取變數,從而不用全域性物件。
原型和原型鏈
function fun(name, age) {
this.name = name + "";
this.age = age;
}
fun.prototype.getName = function() {
return this.name;
};
var fn = new fun();
fn instanceof fun; // true
fn instanceof Object; // true
fn.__proto__ = fun.prototype;
/** fun的原型方法
__proto__ = null
hasOwnProperty = (function)
isPrototypeOf = (function)
propertyIsEnumerable = (function)
toLocaleString = (function)
toString = (function)
valueOf = (function)
*/
重排 reflow 和重繪 repaint
會導致重排重繪的情況:
- 新增或刪除可見元素
- 元素的位置發生改變
- 元素的尺寸發生改變
- 容器內容發生變化導致元素的寬高發生改變
- 瀏覽器視窗初始化和尺寸改變
避免或減少發生重排和重繪的方法:
- 儘可能少的訪問某些變數
// offsetTop、offsetLeft、offsetWidth、offsetHeight
// scrollTop、scrollLeft、scrollWidth、scrollHeight
// clientTop、clientLeft、clientWidth、clientHeight
function scroller() {
var H = document.body.offsetHeight || scrollHeight;
return function() {
var rgs = arguments,
ct = this;
// you core
};
}
- 字串或陣列.join(’’) innerHTML 方式
- createElement 最後 appendChild
- document.createDocumentFragment,cloneNode 需要改變的節點到快取節點中,改完替換
function move2RB() {
var dom = document.getElementById("id"),
curent = dom.style.top;
while (curent < 500) {
curent++;
dom.style.cssText = "left:" + curent + "px; top:" + curent + "px";
// 不要寫成每次都去獲取,left=dom.style.left再加1,甚至是dom.style.left = (pareSint(dom.style.left,10)+1)+'px'這種寫法,直接改變className也是可以的。
}
}
總的來說:少訪問 DOM,在 js 裡處理計算完了再一次性修改,善用快取和原生 API;用現在的三大框架(angular、react、vue)即可不用操心這些
演算法和流程控制
迴圈
- 倒序迴圈 for(var i=10;i>0;i–){}
- 後置迴圈 do{}while(i++<10)
- for-in
條件判斷
-
switch 代替 if-else
-
三目運算
-
判斷可能性從大到小
-
將字串、變數、方法存到陣列或物件中
function getweek() {
var w = ["日", "一", "二", "三", "四", "五", "六"],
now = new Date(),
d = now.getDay();
return "星期" + w[d];
}
遞迴
// 階乘
function facttail(n) {
if (n == 0) {
return 1;
} else {
return n * facttail(n - 1);
}
}
console.log(facttail(5, 1));
// 冪次方
function fn(s, n) {
if (n == 0) {
return 1;
} else {
return s * fn(s, n - 1);
}
}
console.log(fn(2, 3));
利用閉包快取資料
某個方法內部可以儲存計算過的資料或變數:
function memfacttail(n) {
if (!memfacttail.cache) {
memfacttail.cache = {
"0": 1,
"1": 1
};
}
if (!memfacttail.cache.hasOwnProperty(n)) {
memfacttail.cache.n = n * memfacttail(n - 1);
}
return memfacttail.cache.n;
}
console.log(memfacttail(4)); // 4*3*2*1 = 24;