從頁面載入到數據請求,前端頁面性能優化實踐分享

轉載請註明出處:葡萄城官網,葡萄城為開發者提供專業的開發工具、解決方案和服務,賦能開發者。

背景

做過前端開發都知道前端的工作內容是很多的,對於HTML、CSS、Javascript、Image、Flash等各種內容的使用。為了更好提升應用的性能,我們需要對各種資源內容進行不同方面的優化。

對用戶而言,優化可以讓應用的響應速度加快,載入更加迅速,可以帶來更好的使用體驗。
對於服務商而言,前端優化能夠減少頁面請求數量,寬頻所佔頻寬,有效的節省資源。

前端優化的內容很多,按照粒度等級劃分可以大致分為兩類:頁面優化級別和程式碼級別優化。
頁面優化主要針對頁面載入環節,包括:HTTP請求數、腳本的無阻塞載入、內聯腳本的位置優化等內容。程式碼優化包括:Javascript中的DOM操作優化、CSS選擇符優化、圖片優化以及HTML結構優化等內容。

程式碼級別優化則更關注數據請求,很重要的一條就是減少HTTP請求的數量。一個完整的HTTP請求需要經過路由查找,TCP握手,發送請求,伺服器響應和瀏覽器接收等一些列過程。對於小文件,實際下載文件的時間對於整個請求的時間佔比很低,因此需要將小文件合併為大文件來傳輸。

1.webp

(圖片來自網路)

頁面級別:提升頁面載入速度

載入優化是為了解決頁面內容載入速度受限於網路頻寬,過於耗時的問題,主要手段有:
項目打包優化
Webpack 是一個前端資源載入/打包工具。它將根據模組的依賴關係進行靜態分析,然後將這些模組按照指定的規則生成對應的靜態資源。通常我們使用Webpack將多種靜態資源js、css、less 轉換成一個靜態文件,減少了頁面的請求。
核心概念有:
Output:告訴 webpack 在哪裡輸出它所創建的 bundles,以及如何命名這些文件,默認值為 ./dist。
Module:Webpack 會從配置的 Entry 開始遞歸找出所有依賴的模組。
Chunk:一個 Chunk 由多個模組組合而成,用於程式碼合併與分割。
Loader:loader 可以將所有類型的文件轉換為 webpack 能夠處理的有效模組,然後你就可以利用 webpack 的打包能力,對它們進行處理。
Plugin:被用於轉換某些類型的模組,而插件則可以用於執行範圍更廣的任務。

雪碧圖(CSS Sprite)
CSS雪碧 即CSS Sprite,也有人叫它CSS精靈,是一種CSS影像合併技術,該方法是將小圖標和背景影像合併到一張圖片上,然後利用css的背景定位來顯示需要顯示的圖片部分。

雪碧圖實現的基本原理是把我們從網上用到圖片整合在同一張圖片中,從而可以減少網站HTTP的請求數量。這一張圖片使用CSS background和background-position屬性渲染,
這意味著我們的標籤變得更加複雜,圖片是在CSS中定義,而非標籤。

使用雪碧圖有兩個明顯的優點:

  1. 降低網頁圖片內容對伺服器的請求次數
    雪碧圖可以合併大多數的背景圖片和小圖標,方便我們在任何位置使用。不同位置的請求只會調用同一個圖片,大大減少頁面對伺服器的請求次數,降低伺服器的壓力;這樣也可以提高頁面的載入速度,節約伺服器的流量。

  2. 提升頁面載入速度
    雪碧圖拼接的圖片尺寸明顯小於所有圖片拼合之前的打小。
    從這兩方面可以明顯對前端請求速度進行優化。
    在HTTP2之後,已經不需要考慮減少請求數,故雪碧圖現在在前端頁面優化性能的意義已經不大。現在更加推薦使用字體圖標,文件很小並且是矢量圖標
    CDN加速
    CDN的全稱是Content Delivery Network,即內容分發網路。其目的是通過在現有的Internet中增加一層新的CACHE(快取)層,將網站的內容發布到最接近用戶的網路」邊緣「的節點,使用戶可以就近取得所需的內容,提高用戶訪問網站的響應速度。從技術上全面解決由於網路頻寬小、用戶訪問量大、網點分布不均等原因,提高用戶訪問網站的響應速度。

Cache層技術可以用來消除峰值數據訪問造成的節點設備阻塞。Cache伺服器具有快取功能,絕大部分的網頁對象的重複訪問不需要從原始網站重新傳送文件,只需要通過簡單認證將副本發送即可。快取伺服器的位置通常不輸在用戶端附近,所以可以獲得區域網的響應速度,有效減少廣域寬頻消耗。
對於提升響應速、節約頻寬、有效減輕源伺服器的負載十分有效。

總結來說CDN對網路的優化作用主要體現在如下幾個方面: 

  • 解決伺服器端的「第一公里」問題  
  • 緩解甚至消除了不同運營商之間互聯的瓶頸造成的影響  
  • 減輕了各省的出口頻寬壓力  
  • 緩解了骨幹網的壓力  
  • 優化了網上熱點內容的分布

gzip壓縮
Gzip是GNUzip的縮寫,是一個GNU自由軟體的文件壓縮程式,在使用中基本可以壓縮50%的文本文件大小。在說Gzip之前,我們先介紹一個概念,HTTP 壓縮。HTTP 壓縮是一種內置到網頁和網頁客戶端中以改進傳輸速度和頻寬利用率的方式。在使用 HTTP 壓縮的情況下,HTTP 數據在從伺服器發送前就已壓縮:兼容的瀏覽器將在下載所需的格式前宣告支援何種方法給伺服器;不支援壓縮方法的瀏覽器將下載未經壓縮的數據。
HTTP 壓縮就是以縮小體積為目的,對 HTTP 內容進行重新編碼的過程。
Gzip就是HTTP壓縮的經典例題。

減少文件大小會帶來兩個明顯的好處:

  1. 減少存儲空間
  2. 通過網路傳輸時可以減少傳輸時間

Gzip 壓縮背後的原理,是在一個文本文件中找出一些重複出現的字元串、臨時替換它們,從而使整個文件變小。也正是因為這個原理,文件中程式碼的重複率越高,Gzip壓縮的效率就越高,使用 Gzip 的收益也就越大。反之亦然。

項目打包優化

2.jpg

(圖片來自網路)

Webpack 是一個前端資源載入/打包工具。它將根據模組的依賴關係進行靜態分析,然後將這些模組按照指定的規則生成對應的靜態資源。通常我們使用Webpack將多種靜態資源js、css、less 轉換成一個靜態文件,減少了頁面的請求。
核心概念有:
Output:告訴 webpack 在哪裡輸出它所創建的 bundles,以及如何命名這些文件,默認值為 ./dist。
Module:Webpack 會從配置的 Entry 開始遞歸找出所有依賴的模組。
Chunk:一個 Chunk 由多個模組組合而成,用於程式碼合併與分割。
Loader:loader 可以將所有類型的文件轉換為 webpack 能夠處理的有效模組,然後你就可以利用 webpack 的打包能力,對它們進行處理。
Plugin:被用於轉換某些類型的模組,而插件則可以用於執行範圍更廣的任務。

雪碧圖(CSS Sprite)

3.webp

(圖片來自網路)

CSS雪碧 即CSS Sprite,也有人叫它CSS精靈,是一種CSS影像合併技術,該方法是將小圖標和背景影像合併到一張圖片上,然後利用css的背景定位來顯示需要顯示的圖片部分。

雪碧圖實現的基本原理是把我們從網上用到圖片整合在同一張圖片中,從而可以減少網站HTTP的請求數量。這一張圖片使用CSS background和background-position屬性渲染,
這意味著我們的標籤變得更加複雜,圖片是在CSS中定義,而非標籤。

使用雪碧圖有兩個明顯的優點:

  1. 降低網頁圖片內容對伺服器的請求次數
    雪碧圖可以合併大多數的背景圖片和小圖標,方便我們在任何位置使用。不同位置的請求只會調用同一個圖片,大大減少頁面對伺服器的請求次數,降低伺服器的壓力;這樣也可以提高頁面的載入速度,節約伺服器的流量。
  2. 提升頁面載入速度
    雪碧圖拼接的圖片尺寸明顯小於所有圖片拼合之前的打小。
    從這兩方面可以明顯對前端請求速度進行優化。
    在HTTP2之後,已經不需要考慮減少請求數,故雪碧圖現在在前端頁面優化性能的意義已經不大。現在更加推薦使用字體圖標,文件很小並且是矢量圖標

CDN加速

4.jpg

(圖片來自網路)

CDN的全稱是Content Delivery Network,即內容分發網路。其目的是通過在現有的Internet中增加一層新的CACHE(快取)層,將網站的內容發布到最接近用戶的網路」邊緣「的節點,使用戶可以就近取得所需的內容,提高用戶訪問網站的響應速度。從技術上全面解決由於網路頻寬小、用戶訪問量大、網點分布不均等原因,提高用戶訪問網站的響應速度。

Cache層技術可以用來消除峰值數據訪問造成的節點設備阻塞。Cache伺服器具有快取功能,絕大部分的網頁對象的重複訪問不需要從原始網站重新傳送文件,只需要通過簡單認證將副本發送即可。快取伺服器的位置通常不輸在用戶端附近,所以可以獲得區域網的響應速度,有效減少廣域寬頻消耗。
對於提升響應速、節約頻寬、有效減輕源伺服器的負載十分有效。

總結來說CDN對網路的優化作用主要體現在如下幾個方面: 

  • 解決伺服器端的「第一公里」問題  
  • 緩解甚至消除了不同運營商之間互聯的瓶頸造成的影響  
  • 減輕了各省的出口頻寬壓力  
  • 緩解了骨幹網的壓力  
  • 優化了網上熱點內容的分布

gzip壓縮

5.jpg

(圖片來自網路)

Gzip是GNUzip的縮寫,是一個GNU自由軟體的文件壓縮程式,在使用中基本可以壓縮50%的文本文件大小。在說Gzip之前,我們先介紹一個概念,HTTP 壓縮。HTTP 壓縮是一種內置到網頁和網頁客戶端中以改進傳輸速度和頻寬利用率的方式。在使用 HTTP 壓縮的情況下,HTTP 數據在從伺服器發送前就已壓縮:兼容的瀏覽器將在下載所需的格式前宣告支援何種方法給伺服器;不支援壓縮方法的瀏覽器將下載未經壓縮的數據。
HTTP 壓縮就是以縮小體積為目的,對 HTTP 內容進行重新編碼的過程。
Gzip就是HTTP壓縮的經典例題。

減少文件大小會帶來兩個明顯的好處:

  1. 減少存儲空間

  2. 通過網路傳輸時可以減少傳輸時間

Gzip 壓縮背後的原理,是在一個文本文件中找出一些重複出現的字元串、臨時替換它們,從而使整個文件變小。也正是因為這個原理,文件中程式碼的重複率越高,Gzip壓縮的效率就越高,使用 Gzip 的收益也就越大。反之亦然。

程式碼級別:減少數據請求次數

前面我們列舉了在頁面初始載入時的優化方法,然而在某些場景下這還不夠,因為經常會出現頁面展示和使用時,頻繁請求服務來更新資訊的場景。

例如在開發類Excel在線協同系統時,因為單元格業務相互獨立,全螢幕刷新無法滿足需求。我們只能定時從伺服器獲取每個單元格的值,檢測到變化後展示在頁面上。而每個單元格分別調用api獲取內容,就會產生大量網路請求。大量的請求一方面拖累了載入速度,頁面也會發生卡頓。

6.gif

在這種場景下,WebSocket是一個很好的選擇,通過長鏈接的方式保持與伺服器的同步,服務端主動推送更新到客戶端,減少了網路的開銷。但是WebSocket也有自身的缺點,開發成本高,無論是客戶端還是服務端都需要考慮斷開重連、頻繁推送、資源佔用等問題。所以,我們還需要通過優化,盡量減少請求頻率。

優化思路

如何減少數據請求數量?我們可以通過請求隊列的方式,對邏輯進行優化。

7.png

(通過請求隊列優化Web請求)

經過優化,類Excel在線協同系統獲取數據的邏輯變成了如下的樣子:

  • 當單元格發送請求時,請求先添加ID,並通過ID快取callback方法,然後進入請求隊列,隊列管理器定時或者根據隊列中請求數量多少像服務端發送請求包。
  • 服務端接收到請求包後批量處理,處理後封裝新的返回包
  • 前端接受到返回包後根據請求的唯一ID,調用對應的callback方法執行,完成單元格的請求
    使用此方法進行優化,優點是顯而易見的:
  • 實現簡單,程式碼改動小,原本的ajax請求改為隊列調用即可,請求後的callbak無需修改。服務端添加一個新介面拆分請求即可。
  • 根據實際場景設置請求頻率或者一次請求中數據的數量,兼顧更新頻率和相應次數。

應用實例

下面程式碼是GETNUMBERFROMSERVER的實現,該函數負責調用伺服器的getData介面,傳遞參數,獲取顯示內容並展示在單元格。為了確保非同步更新單元格的用戶體驗,這個函數源自SpreadJS的非同步函數。

1.	    var GetNumberFromServer = function () {
2.	    };
3.	    GetNumberFromServer.prototype = new GC.Spread.CalcEngine.Functions.AsyncFunction("GETNUMBERFROMSERVER", 1, 2);
4.	    GetNumberFromServer.prototype.evaluate = function (context, arg1, arg2) {
5.	    fetch("/spread/getData?data="+arg1)
6.	    .then(function(response) {
7.	        return response.text();
8.	    })
9.	    .then(function(text) {
10.	        context.setAsyncResult(text);
11.	    });
12.	    };
13.	    GC.Spread.CalcEngine.Functions.defineGlobalCustomFunction("GETNUMBERFROMSERVER", new GetNumberFromServer());
14.	

為了減少請求,我們首先需要使用一個快取對象存放請求數據,定時調用介面處理。

1.	let callStack = {}; //收集請求數據
2.	let callingStack = {}; //快取正在請求中的數據資訊
3.	let callStackCount = 0; //請求數量,當作請求ID,用於區分請求內容
4.	let timingId = 0; //用於判斷當前是否有定時器等待請求中


然後,我們定義新的隊列化請求方法,代替在函數中直接調用API介面。

1.	
2.	// data 請求數據
3.	// context 非同步函數context, 網路請求結束後回調時使用
4.	// callback 回調函數
5.	function stackCall(data, context, callback){
6.	    let id = callStackCount++;
7.	    callStack[id] = {};
8.	    callStack[id].data = data;
9.	    callStack[id].context = context;
10.	    callStack[id].callback = callback;
11.	
12.	    if(timingId === 0){ // 同時只有一個定時器
13.	        timingId = setTimeout(function(){
14.	            callingStack = callStack;
15.	            callStack = {};
16.	
17.	            let newData = "" //合併請求數據,根據實際業務情況整理
18.	            for(let cId in callingStack){
19.	                newData += (cId + "," + callingStack[cId].data + ";");
20.	            }
21.	
22.	            // 發送請求,這裡模擬數據,發送什麼返回什麼
23.	            fetch("/spread/getData?data=" + newData)
24.	                  .then(function(response) {
25.	                    return response.text();
26.	                  })
27.	                  .then(function(text) {
28.	                    let resData = newData.split(";");
29.	                    let spread = designer.getWorkbook();
30.	                    spread.suspendPaint(); //暫定頁面繪製
31.	
32.	                    //解析返回的數據
33.	                    for(let resId in resData){
34.	                        if(resData[resId]){
35.	                            let ress = resData[resId].split(",");
36.	                            // 根據Id,獲取函數的context,調用callback回調
37.	                            callingStack[ress[0]].callback.call(null, callingStack[ress[0]].context, ress[1])
38.	                        }
39.	                    }
40.	                    spread.resumePaint(); //重啟統一繪製
41.	                timingId = 0;
42.	            });
43.	        }, 1000)
44.	    }
45.	}


最後更新非同步函數的實現方式,在函數中調用stackCall堆棧函數,批量調用成功後執行callback回調中的setAsyncResult方法,最終實現業務邏輯。

1.	GetNumberFromServer.prototype.evaluate = function (context, arg1, arg2) {
2.	    stackCall(arg1, context, function(context, text){
3.	        context.setAsyncResult(text);
4.	    })
5.	    };



經過這次優化,當頁面有大量非同步請求時,這些請求會放到隊列中,定時統一處理,一次刷新。

此外,我們還可以使用SpreadJS的doNotRecalculateAfterLoad導入選項,在首次載入時不計算,改用json中原始值;以及calcOnDemand開啟按需計算。進一步優化頁面初始化的速度和體驗。


1.	json.calcOnDemand = true;
2.	spread.fromJSON(json, { doNotRecalculateAfterLoad: true });

總結

本文分類介紹了幾種前端性能優化的方法。這些最佳實踐覆蓋了頁面載入和數據請求環節。在文章的後半部分,我們通過類Excel在線協同編輯的實例,詳細介紹了「數據請求隊列化」的實現,希望對您的前端開發有幫助。