1. 程式人生 > >前端優化常用技術

前端優化常用技術

前端優化常用技術

從建立 http 連線開始,到頁面展示在瀏覽器中,一共經過了載入,執行,渲染,重構等幾個階段。

載入

PV(訪問量):即 Page View,頁面重新整理一次算一次。也叫併發量。

UV(獨立訪客):即 Unique Visitor,一個客戶端(電腦,手機)為一個訪客

比如:百萬級的 PV,併發量過大怎麼辦 ?

由此,前端所有的優化都是基於這個點和單執行緒而延伸出來的。所以,前端的資源載入優化有兩個方向:

  1. 開源

增加域名,簡單來說就是 cdn。

  1. 節流

資源壓縮,去除空格,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>

## 執行優化

作用域優化

  1. 變數層級不要巢狀太深。
(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
  }
}

迴圈優化

  1. 簡化終止條件
for (var i = 0; i < values.length; i++) {
  process(values[i]);
}

for (var i = 0, len = values.length; i < len; i++) {
  process(values[i]);
}
  1. 展開迴圈
// 針對大資料集使用展開迴圈可以節省很多時間,但對於小資料集,額外的開銷則可能得不償失。

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

最小化語句數

  1. 多個變數宣告
// 避免
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();
  1. 插入迭代值
// 避免
var name = values[i];
i++;

// 提倡
var name = values[i++];
  1. 使用陣列和物件字面量,避免使用建構函式 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
};

效能的其它注意事項

  1. 原生方法更快

用諸如 C/C++之類的編譯型語言寫出來的,要比 JS 的快多了。

  1. 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;