背景 項目 :移動端H5電商項目 痛點 :慢!!! 初始方案 :最基本的圖片懶載入,靜態資源放到cdn,predns等等已經都做了。但是還是慢,慢在哪? 顯而易見的原因 :由於前後端分離,所有的數據都由介面下發,之後根據模板渲染頁面。也就是說,我們需要先載入js,等到js載入完畢之後,請求介面,介面 ...
背景
項目:移動端H5電商項目
痛點:慢!!!
初始方案:最基本的圖片懶載入,靜態資源放到cdn,predns等等已經都做了。但是還是慢,慢在哪?
顯而易見的原因:由於前後端分離,所有的數據都由介面下發,之後根據模板渲染頁面。也就是說,我們需要先載入js,等到js載入完畢之後,請求介面,介面返回數據之後,渲染頁面,載入圖片等等。儘管使用了模塊化的載入方式,但是對於要求高的首頁和活動頁,給用戶的感知也不是很好。
初版解決方案
最初,由於時間緊迫,基本上都是從客戶端作優化處理,基本上可以總結為以下幾個方面。
一、本地緩存
我們做了本地緩存優化的策略,第一次請求之後就把介面數據緩存到localStorage裡面,並且存儲當時的時間,設定過期時間,一般設置為5分鐘,用戶在5分鐘內重覆打開頁面,不會再次請求介面,從localstorage中拿取數據,直接渲染頁面。
後續乾脆把模板渲染好的html片段存儲了起來,直接拼接,省去了模板計算的時間。
基本實現方案如下:
var
cache = localStorage.getItem('cache')
, expires = 5 * 60 * 1000
;
// 判斷是否過期
function isOverdue(pastTime, expires) {
return Date.now() - pasttime >= expires;
}
if (cache && !isOverdue(cache.time, expires)) {
// 說明緩存存在,並且沒有過期
// 就正常取cache.data做相應的渲染
} else {
// 說明緩存不存在或者已經過期了
// 重新請求介面
$.get('a.cn', funciton (res) {
// do something
// 把對應的渲染操作處理完成之後,將數據緩存,並記錄當前的時間
localStorage.setItem('cache', {
data: res,
time: Date.now()
})
})
}
然而還是不夠,新用戶在首次打開時,還是不能秒開頁面,並且用戶在5分鐘之後重新載入之時,仍然會有一定的延遲(由於瀏覽器會緩存一部分靜態資源,此時再打開並不會像用戶初次打開一樣那麼慢)。
二、進一步緩存靜態資源
在日常開發中,有很多依賴庫,常用的fastclick,swipe等等,這些庫,沒有必要每次都去載入,雖然瀏覽器會對一些靜態資源做緩存,但是卻不能完全被我們控制,所以,可以將這些不常發生變化的靜態資源緩存起來,同樣的,存到localStorage裡面。
需要註意
這個方案有一個問題,如果是直接載入的script標簽,是無法直接拿到它的腳本內容的。
<script id="script1" src="js/jquery.js"></script>
console.log(document.querySelector('#script1').innerHTML);
// 此時輸出的是undefined,因為innerHTML是獲取標簽內容,此時script標簽里並沒有內容。
<script id="script2">
console.log('2');
</script>
console.log(document.querySelector('#script2').innerHTML);
// 此時輸出的是console.log('2');
// 因為innerHTML是獲取標簽內容,此時script標簽里並沒有內容。
這當然不是我們想要的,我們需要的是外鏈js的可執行代碼。
動態添加js的兩種方案
我在前一篇高性能JavaScript讀書筆記中提到了兩種方案。
1. 動態腳本元素
var script = document.createElement('script');
script.type = 'text/javascript';
script.onload = function () {
// do something
}
script.src = 'jquery.js';
document.getElementByTagName('heda')[0].appendChild(script);
這種方法可以監控到腳本的完成事件,但是由於也是通過添加一個script標簽,並不能拿到我們想要的js代碼。
2. 通過XMLHttPRequest腳本註入
var xhr = new XMLHttpRequest();
xhr.open("get", "file1.js", true);
xhr.onreadystatechange = function () {
if (xhr.readyState == 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
localStorage.setItem('file1', xhr.responseText);
}
}
}
需要特別註意的是,這個方法有跨域的風險,所以,我們需要靜態資源伺服器的allow-origin設置為*。或者直接載入本功能變數名稱下的js。
有同學要問了,既然localStorage這麼強大,為什麼不把所有的東西都緩存起來呢?
當然是因為它的大小有限制,在FireFox和chrome中,一般來說,sessionStorage和localStorage大小為10MB,而safari只有5MB。詳見這篇文章。
這就限制了我們什麼都存的想法。如果圖片較小,可以緩存起來。
第二版解決方案
在前一版方案里,我們解決了後續載入的速度緩慢問題,在後續的頁面打開速度上,基本上可以做到秒開。
但是這個方案還是有不足,對於新用戶的體驗不是很好,如果用戶在5min的這個時間點上打開,速度還是會有所下降。
最後還是只能做SSR。
知乎上有一個問題,為什麼現在又流行服務端渲染html?。
服務端渲染有很多好處,對我們此時而言,最大的好處就是頁面直出。省去了請求介面這一步操作。並且對於提高用戶體驗上來說,很有好處。
如果時間充裕,可以使用node做服務端,但是由於歷史原因,我們這個項目遷移起來也比較費時間,所以最後決定使用openresty來做。
openResty的教程網上很多了,我也不多說,除了官方git,推薦開濤博客學習。
不論我們是在客戶端取介面數據,還是服務端,活動頁的數據一般來說都有一定持續時長,也就是說,我們也可以在我們的nginx伺服器上做一個緩存,假設有一個用戶訪問了一個活動,那麼在接下來的五分鐘之內,其他任何用戶(相同許可權下)訪問到的就是第一個用戶訪問時緩存好的頁面。
local args = ngx.req.get_uri_args()
local acId = args['acId']
local key = 'ac' .. acId
local expire = 5 * 60 --緩存時間5分鐘
local value = dict.getData(key, expire)
if not value then
local res = ngx.location.capture('/a.json')
if res.status == 200 then
-- dict是一個用來處理存儲和讀取邏輯的腳本
dict.setData(key, res.body)
else
ngx.say(dict.getData(key))
end
else
-- ngx.log(4, type(value))
ngx.say(value)
end
在dict中,我們可以把介面數據轉換成通過模板轉換為html片段存儲起來。這樣,用戶在第一次進入我們的頁面以後,也不會感覺慢了。
what's more
openResty能做的事不僅僅是這些,一些網關上許可權的控制等等都可以用它來實現。
上面兩個方案還有一些不足之處,比如首屏可能內容比較多,一次都載入過來,不一定會快。直觀的首屏的由服務端渲染,對於不重要的內容,可以通過AJAX來非同步載入。
如何計算活動頁這樣的頁面中,首屏有多大,如何組織代碼,哪些部分採用服務端渲染,哪些採用AJAX。這些問題在實際開發中也需要考慮。
限於時間,此次沒有能比較完善的解決這個問題,在日後開發中,還會繼續完善活動頁優化方案。