網站為什麼 JS 調用盡量放到網頁底部?
因為瀏覽器渲染HTML文件是從上往下渲染的。
即先執行head標籤里的內容,再執行body標籤里的,一行行渲染下去。無論當前 JavaScript 代碼是內嵌還是在外鏈文件中,頁面的下載和渲染都必須停下來等待腳本執行完成。JavaScript 執行過程耗時越久,瀏覽器等待響應用戶輸入的時間就越長。
所以JS盡量放底部可以有一定的性能優化效果。
題外話——關於JavaScript性能優化:
除了上面這個方法,還可以通過設置script標籤的defer或async屬性、合併腳本等等方法來優化。推薦一篇乾貨:JavaScript 的性能優化:載入和執行還有個可訪問性的考慮:如果 &
-----------
一、為什麼我會認為這是個好問題?!在我帶過的幾個電商項目前端團隊中,就因為這個問題開過幾個不開竅的前端。
理由很簡單,也比較霸道。這幾天我們項目又在做前端性能優化,又有同事拿著這個疑惑來問我,呵呵,不會再亂開人了。
以前為此開人,可能是我沒能力用通俗易懂的文字來描述好這個問題,難免有誤傷。為了避免這樣的事情,我決定要好好把問題說清楚,就從瀏覽器渲染的原則開始!
二、是不是網頁JS調用都盡量放到網頁底部?
按知乎的原則,先要問是不是,再問為什麼,但這個"是不是"對於做前端技術的人來說,一眼就能看出個所以然。
顯然,不是的!但性能優化做得好的網站基本上是這個原則。給兩個我認為前端優化做得比較好的知名海淘類電商,大家去觀摩一下別人的做法:
-----------
網易考拉海購!貝貝網
-----------當然,我們不能用Bigpipe這種極端的優化技術流派來解答,或許是解答不了的,但即便是bigpipe其背後的原理也是和這個原則並沒有衝突的,大道至簡,底層的原理是一樣的,只是它盡全力地利用每一次http請求,用前端的技術手段來載入足夠多的資源。
當然了,題主應該是一個前端開發,而且應該是在碰到頁面優化需求或學習上的疑惑了,可能有人強逼這TA按照這樣的原則去做,但又不知道什麼別人為啥要求他這麼做,於是就有了這個問題!
至於情況是不是這樣的,我只是瞎猜的。但是,我帶的前端團隊裡面有不少同學存在同樣的疑惑,因為我有這樣一個規範:
不知道有多少團隊有這樣的標準化demo模板規範?這是我1年多以前做的了,在這裡——基於gulp的前端框架開發規範。大家可以去我的博客看看,權當參考。當然,現在這個規範已經有了新版本。--------------------
三、為什麼網頁JS調用都盡量放到網頁底部?這部分如果《高性能網站建設指南》這本書上有詳細說明的並且我也認同的,就直接截圖貼過來,這裡我只說自己的理解部分。還有,對於書本上的東西,我的態度是——盡信書,不如無書。
(一)大家的做法是不是一樣的?
ok,我先把問題分解一下。
首先這裡有個關鍵詞——盡量。《高性能網站建設指南》這本書裡面用的詞是「如果可以的話」。也就是說,js不完全是一定要放在頁面底部的,但是你要了解清楚以下兩個問題了:- 什麼是盡量的(可以的)那部分js代碼?
- 什麼是不盡量的(不可以的)那部分呢?
只有了解清楚這兩個問題,你才知道如何去安排js在頁面中的位置。ok,我們來看看考拉網和貝貝網的首頁源代碼。
- 考拉網:&&之間就一段讓IE9以下瀏覽器兼容HTML5標籤的js代碼,這是一個底層的兼容腳本,不涉及任何頁面邏輯,而它的全部頁面邏輯都是放置在腳步。
- 貝貝網:&&之間放置的是一些全局設置和一些統計腳本,也不涉及頁面邏輯,邏輯部分js也是放在頁面底部。
- 我們項目的:做法是兩者的結合,heah標籤內就的js腳本就只是定義幾個全局的命名空間和一段統計腳本,沒了,而業務邏輯js就放置頁面最底部。
這是我們的PC端js的大致布局。考拉網也是類似分配方式,只是把core和common合併在一起(超過了200K),我們沒有。我是覺得合併在一起這樣模塊太大了,不太利於弱網用戶,但多一次http請求,有利有弊吧。
有部分人可能會問,為啥不統統合併在一起,就1個HTTP請求了?我只能呵呵,網站不是只有1個首頁,還有很多其他頁面呢,只要把js底層庫和常用的公共類庫載入一次,其他頁面就可以被緩存起來(form cache或304)這個前端技術表面的對比,是不是有點意思呢?
我們幾個都是海淘類垂直電商,只是定位和強勢品類有些小差別,但在業務層面其實是基本相同的,都是海淘。也就是說,我們互為競爭對手!那麼,很顯然我們不可能相互溝通、開會,然後通報我用什麼前端架構,前後端協作開發的模式等等技術問題,但是為什麼做法上大家是那麼的一致呢?
這就是問題了!我絕對保證不認識考拉的前端架構設計師,但我的前端設計方案出來的結果幾乎是一模一樣的!為什麼呢?
簡單點說,條條道路通羅馬。就這樣,沒為什麼。後面我匯以網易考拉為案例,逆向分析他們的做法,進而嘗試窺探他們的前端架構設計方案。不一定正確,只是個人看法。
如果你想得到絕對正確的答案,就想辦法進入裡面,或者發一個類似這樣的問題(美團的前端架構是怎樣的? - 前端開發),看看有沒有人出來回復。
-------2015年09月10日22:56:43---------
這裡上一個簡單的web優化對比圖(阿里測,專業的網站即時探測工具):
對比只是一個參考,不見得我們的技術開發人員實力很好,很多就是一般水平的。而且電商的競爭很多時候不僅僅是技術力量的角力,還有產品理念,運營能力,市場推廣等等綜合因素決定的,技術只是基礎,特別是後進場的玩家,我們的項目上線才4個多月,我不想透露太多,避免中槍。這裡是顯擺的,我們項目最近優化的結果:這次的優化心得:由於我們運營增加了2個第三方統計,是的我們對第三方靜態內容緩存控制力度下降了,但是我們改善了gzip和圖片的壓縮優化,比前幾天提高了幾分,但我認為還有優化空間,比如:- css用到的雪碧圖的壓縮比例還不夠,需要改進前端構建框架,改進圖片壓縮的演算法,下個星期上一個行版本,看看效果
- html文檔並沒有迷你化,要將服務端的模板弄到前端構建流程裡面,控制起來做壓縮,這個有利有弊,利就是可以在發布到服務端前作壓縮,弊端就是迷你化後不太方便調試
這兩點弄好了,上90分應該不成問題。如果也是做這一塊的,可以將經驗分享出來,相互學習。我們和考拉網的代碼量幾乎一致,交互也基本相同,有一定對比價值。
----------
這裡必須補充一點:用類似阿里測這種工具來對比,只能作為一種參考,如果大家的業務不同,頁面的Dom數量差異太多,且交互場景也有很大差別,那麼這種對比是沒有任何意義的。----------(二)js在頁面中不同位置帶來的影響或效果區別
(這裡將是講瀏覽器資源載入原理和js執行原理的,用通俗易懂的方式說明白需要死掉很多腦細胞,查閱很多很多資料,可能包括已經還給老師的E文,會慢一點點。我爭取說得通俗易懂,但發現很難,大家要有心理準備。)
要徹底搞懂,為什麼別人建議js放在頁面的底部,那麼我需要從js的語言機制及其運行環境說起。
1,瀏覽器不是單線程的,它多線程的,如果有必要它還是多進程的。
很多同學並不理解瀏覽器不是單線程的,別問我為什麼,暫時不打算做代課老師,這裡有兩篇,自己去理解。
瀏覽器是怎樣工作的(一):基礎知識
瀏覽器是怎樣工作的:渲染引擎,HTML解析(連載二)例如,Webkit或是Gecko引擎,都可能有如下線程:- javascript引擎線程
- 界面渲染線程
- 瀏覽器事件觸發線程
- Http請求線程
2,js是單線程的
證明的腳本:
在1萬次循環迭代過程中,foo()一直先列印了1萬次『first』,定時時間執行時間為0,它也不去執行裡面的mylog函數,而是等待循環結束後,再輸出1萬次『second』,看起來像運行了兩次迭代,是不是表現很怪異?為什麼?
因為JS運行在瀏覽器中,是單線程的,每個瀏覽器頁面就是一個JS線程,既然是單線程的,在某個特定的時刻只有特定的代碼能夠被執行,並阻塞其它的代碼。而瀏覽器是多線程的,它又一個名叫Event driven(事件驅動)的線程,而且瀏覽器具備Asynchronized(非同步)執行事件的特性,會創建事件並放入執行隊列中,非同步執行。
瀏覽器定義的非同步事件有很多種,例如mouse click(滑鼠點擊事件), a timer firing(定時器觸發事件), 或者an XMLHttpRequest completing(XMLHttpRequest完成回調事件),一旦js代碼中有這樣的事件代碼,瀏覽器就會將它們放入執行隊列,等待當前js代碼執行完成之後,再按隊列情況逐個執行。
於是,我們就看到了上面的 setTimeout(mylog, 0); 這段代碼被執行的怪異表現了,也就是mylog的執行順序被改變了。一個有用的知識點: setTimeout(func, 0)的作用
- 讓瀏覽器渲染當前的變化(很多瀏覽器UI render和js執行是放在一個線程中,線程阻塞會導致界面無法更新渲染)
- 重新評估」script is running too long」警告
- 改變代碼塊的執行順序
3,瀏覽器對資源的載入是線性的,可並行的,但js除外
當我們在瀏覽器的地址欄里輸入一個url地址,訪問一個新頁面時候,頁面展示的快慢就是由一個單線程所控制,這個線程叫做UI線程,UI線程會根據頁面里資源(資源是html文件、圖片、css等)書寫的先後順序,它會按照資源的類型發起http請求來獲取資源,當http請求處理完畢也就意味著資源載入結束。
但是碰到javascript文件則不同,它的載入過程被分為兩步,第一步和載入css文件和圖片一樣,就是執行一個http請求下載外部的js文件,但是javascript完成http操作後並不意味操作完畢,UI線程就會通知javascript引擎線程來執行它,如果javascript代碼執行時間過長,那麼用戶就會明顯感覺到頁面的延遲。
為什麼瀏覽器不能把javascript代碼的載入過程拆分為下載和執行兩個並行的過程,這樣就可以充分利用時間完成http請求,這樣不是就能提升頁面的載入效率了嗎?
答案當然是否定的。
因為javascript是一個圖靈完備的編程語言,js代碼是有智力的,它除了可以完成邏輯性的工作,還可以通過操作頁面元素來改變頁面的UI渲染,如果我們忽略javascript對網頁UI界面渲染的影響,讓它下載和運行是分開的(也可以理解為js代碼可以延遲執行),結果會造成頁面展示的混亂,或多次重繪。很顯然,這樣的做法是不合適的,因此,js腳本的下載和執行必須是一個完整的操作,是不能被割裂的。
百度首頁的資源下載瀑布圖(沒有任何緩存狀態下,所有http請求響應狀態都是200)既然拆分js的下載和執行是不可行的,但為了提升用戶體驗,加快UI線程的執行又是一個無法迴避的問題,於是瀏覽器就換了種方式,讓它在同一個時間可以下載多個資源。
例如上面百度的截圖,在同一個域名下,firefox可以同時下載兩種圖片(chrome可以同時下載4個靜態資源),不過這是針對圖片和css文件,對於js文件似乎還是一個接著一個的下載,下載一個執行一個,不過到了js執行時候還是要嚴格按照順序執行。當然,我在途中用黃色標記的幾個js是並行載入的,其實是先前某個js發起的請求(這種做法就是無阻塞的js載入,也叫非同步載入,後面我會說明這個東西有什麼好壞)。
多個http連接並行下載資源就好比多個線程共同完成某個任務,如果並行http連接更多,那麼能有更多http資源同時被下載,但是瀏覽器提供並行執行的http連接實在太少了,例如上面firefox才兩個,chrome也只有4個,那如何突破瀏覽器的連接個數的限制了?
方法很簡單就是將常用的,穩定的靜態資源統一放在靜態資源伺服器上,由統一的域名對外提供連接,而這個域名要和主域名不一樣就可以了。也就是將靜態資源放在CDN節點上,單獨用一個域名來對應。
到這裡,可能有人會問,是不是給每個靜態資源分配一個域名,讓所有資源都可以並行下載就會達到最佳狀態了呢?
答案當然也是否定的。簡單滴說,有兩個方面原因:
一方面,我們採用的是http1.1版協議,它的特點是資源下載過程是一個長連接,而長連接的好處是在頁面和服務端頻繁交互時效率更好,但http協議有時候不是那麼的可靠,導致伺服器要維護一些無用的長連接,訪問的人次越多越糟糕。另一方面,當我們同時在一個頁面中使用的域名過多,會導致dns解析的開銷增大。
那麼,多少個域是最合適的呢?好像是雅虎軍規中有提過,最佳的建議是2個。也就是圖片一個CDN域,css和js一個域。
4,放到網頁頂部的js就一定阻塞頁面渲染嗎?
剛才說了,js之所以會阻塞UI線程的執行,是因為javascript能影響甚至控制UI渲染的過程,而頁面載入的規則是要順序執行,所以在碰到js代碼時候UI線程就會通知js引擎來執行它。
然而,很早很早以前,很多程序員不知道這個特點或者知道但被忽視,因此導致編寫代碼時候將用於展示的代碼和用於處理邏輯的代碼混淆在一起,這樣做的後果是使js代碼造成的阻塞更加嚴重,於是業界良心的雅虎出台了這個軍規——將js腳本放置到html文檔的末尾。
如果不想深究這個話題,其實到這裡就可以結束了。但是,我個人不是很喜歡按常規套路出牌,比如對於軍規進行一個反問——難道將js腳本放置在head部分就一定會阻塞頁面渲染嗎?
答案其實依然是否定的。我簡單用常用的jQuery.lazyload插件整了兩個簡單的測試
http://sandbox.runjs.cn/show/mute4dfe --&>這個是所有js在頭部的http://sandbox.runjs.cn/show/bnam8lfs --&>這個是所有js在尾部的(這裡請對比源代碼)兩者的效果是一致的,但如果我們把js頭部的lazyload實例化的腳本改成不放在 $(document).ready() 這個方法里,而是直接實例,那麼lazyload就失效了。//原來的
$(document).ready(function() {
$("img.lazy").lazyload();
});
//如果改成這樣,並放在head標籤內部,那麼lazyload就失效
//但是如果我們將這個放在所有的img元素以後,那麼lazyload就又生效了
$("img.lazy").lazyload();
為什麼會這樣?
雖然我們將js全部放在頭部,但事實上是利用jq的一個延遲執行的介面——$(document).ready() ,讓js邏輯( $("img.lazy").lazyload(); )的執行延遲到文檔準備好了之後,因而保障了lazyload在執行的時候,頁面中的img標籤是存在的。
但是,如果不放在這個介面裡面,那麼頭部的js邏輯就會在文檔準備完成之前執行,這時候頁面中還沒有img元素,因此也就失效了。
不過,如果我們將這段實例化的邏輯弄到後面(所有img標籤之後),即便不放在$(document).ready裡面執行,lazyload就又能生效了。
但這個時候,其實這段邏輯依賴的jQuery庫和lazyload插件並不需要放在head裡面了,而只要保持在實例化的邏輯之前一點點就可以了,而這種做法就是雅虎軍規推薦的。
當然,這裡的結論是「放到網頁頂部的js不一定阻塞頁面渲染」,只要將實例化的js介面或方法封裝在$(document).ready介面內,這樣就可以保障邏輯能夠順利進行。也可以這麼寫:$(function(){
$("img.lazy").lazyload();
})
5,為什麼電商網站喜歡將js邏輯放在腳部呢?
很顯然,這並不是海淘電商的前端架構設計者有特殊的癖好,而是業務優化的需要。但是,這個需要結合不同的架構情況來具體分析,如果涉及到需要用前端模板引擎渲染頁面的,情況就更加複雜了。
比如唯品會,大家先去觀摩一下她的首頁,是不是有很多類似這樣的代碼:&
&
&
這就是前端模板引擎的標誌性代碼。那麼,就這種情況,我先提出幾個疑問:
a,請問當頁面有大量這樣的代碼需要處理和維護時,頁面中的js到底怎麼布置才是合理的呢?
b,你不覺得,這樣的模板維護起來是不太容易嗎?直接放置在頁面上,如果這個頁面要經常變動(首頁顯然是變動頻率比較高的),前端後端都可能有人來維護這份代碼,請問衝突了怎麼解決呢?有沒有辦法將衝突風險降到最低?c,請問如果我們不把這些文件直接放在頁面上,比如將它們弄到在某個js裡面,這樣做可以嗎?d,如果要這樣做,如何讓前端開發人員容易維護這樣的代碼,比如保存即可看到效果,需要怎樣一種前端架構設計來完成這需求呢?嚴重聲明:
1. 這裡並不是想拿VIP開刷,而是我真的一時半會找不到更合適、更有代表性的對象了。這些問題涉及前端開發模式選擇的問題,這個章節我不打算展開,留給後面的前端架構分析來補充!2. 雖然我覺得VIP的做法還有更好的選擇,但並不是說換我去實施就一定比現在負責這一塊的同行做得更好,真心不是這樣的。電商這一個領域,很多時候業務的實現都是有時限的,特別是VIP這種高速發展的電商,因此這裡只是一種局外人的視角,隨便說說罷了。
先解決這個問題——
為什麼我會說電商喜歡將js放在頁面的尾部是因為業務優化的需要呢?至少我已經找到了3家(就是前面對比的),因此這不是個例。這裡要搞清楚「電商的業務優化需要」是什麼?首先,我們要知道,電商其實就是和錢打交道的網站,賣東西,然後收錢,本質上和你我他家門口的小賣部沒啥兩樣,只是我們通過網路來進行,而網路這種東西是不透明的,你不知道賣東西的是不是一條狗還是一隻貓,當然買東西的一樣,大家相互信任的基礎是非常薄弱的。
好在淘寶等一大批先驅將網路支付的這種文化或習慣培養了起來,我們後進場的電商玩家就要基於各種在線支付手段來完成買賣,但是玩家人數很多啊,而且都不是BAT級別的,本來用戶對於這種電商的信任度就比較低,大家對於任何可以提高用戶體驗的細節都要做到斤斤計較,能有多完美就要多完美。
還有很多,但必須打住...我只是為了照顧部分客官,打了這麼多廢話出來,其實就是想說明電商的競爭非常激烈,極致的用戶體驗是分出勝負手的關鍵,這就是前端技術發揮餘力的地方了。比如,儘快輸出首屏幕的頁面內容,儘快地讓用戶可以進行交互,等等吧。首頁首屏秒開已經是電商的最低要求了,秒開,你懂吧?
那麼,電商的首頁首屏都有啥內容呢?如何以極限的速度呈現給用戶呢?
咱們是前端,我只能限制在前端的範疇。假設html文檔都是150Kb左右,gzip之後是30Kb左右,網路帶寬一樣,而用戶獲得文檔的時間基本是一致的。在這些前提下,我們同時獲得文檔之後,如何根據html結構布置js及其交互邏輯,才能以最快速度呈現首屏內容並提供交互呢?
這裡就以考拉網為例,其首屏內容如下圖:其首屏內容可細分為- header部分(包括topbar、logo、搜索框和導航)
- slider廣告幻燈片(多圖)
- 四個優勢提醒(網易自營,低價保障,閃電發貨,全場包郵)
- 一個通欄廣告(就是App下載的那個)
由於瀏覽器線性載入的特點,首屏內容看到之前,我們至少會得到文檔內容和css樣式,如果我們第一時間讓用戶看到頁面,那麼,這裡就應該只有首屏的圖片就可以了,比如logo、購物側圖標、banner大圖,四個優勢對應的圖標以及廣告圖片。
如果我們將js布置在head裡面,那麼碰到js就要第一時間去載入js,這裡就要消耗瀏覽器載入資源,對嗎?而且js是阻塞方式載入的,即便是載入足夠快,但也阻擋了首屏的內容!也就是,將js放在這裡是不合適的。
但是,我們將js放到文檔的後面,如果前面有很多很多的圖片資源,那麼瀏覽器發起js載入請求可能就要等這些圖片載入之後,那麼首屏的頁面卻需要js提供交互了,比如幻燈片就是這種情況,那麼首屏之後的內容就不能干擾js的載入,否則用戶體驗就很差,因為發生了TTI延遲問題(這個TTI兄弟,它有很多可以訴說的故事,切莫著急,俺會慢慢道來滴)。
事實上,網易考拉網的首頁首屏內容的呈現的優化空間,至少有兩點:
第1點:
導航裡面有很多「熱賣大牌」,帶有logo的,這些logo圖片是直接用src發請求的,這是完全沒有必要的,因為第一次進來用戶沒有觸發導航下拉之前看不到這些東西,或者他可能從來到這裡到離開,都不會點擊打開下拉列表。比如我就是一個案例,在沒有寫這個文章之前,也不知道導航裡面有那麼多圖片的,研究了源代碼才發現這裡還有那麼多精彩,因此建議用js控制起來,用戶沒有觸發之前就不要發起請求了。
第2點:
首屏內容中輪播大圖在用戶進來之後就全部載入了,這也是沒有必要的,只需要其中的一張即可。也就是,進入頁面後用img的scr發起一個下載請求,其他img不要即刻去發起下載,而是等到js載入之後,用js來發起其他圖片請求,等待所有輪播圖片都載入完成之後,再執行輪播的邏輯。但是,如果要用js控制輪播圖的載入就會有一個技術上的難點,如何知道輪播圖片載入完成了呢?是全部完成之後再提供輪播切換交互,還是出現載入完一張就多一個切換,抑或是當載入到第n張之後就開始提供可交互,而不管n張以後的是否載入完成呢?此外,如果是要兼容IE6/7/8,坑很多。
按照這個思路,在兼顧SEO的前提下,PC端電商首頁首屏要以極限的速度呈現給用戶,我的做法是:- 也就是說,電商網站PC端做極限的性能優化,其精髓就是——將瀏覽器基本無序的資源載入請求有序地控制起來,包括js本身,這就是js要放在腳步的緣由。
- 除了首屏看得見的資源(主要是圖片資源)外,其他資源一律需要通過js來控制,而不能隨意地發起http請求(包括首屏看不到的資源)。按照這個原則,js只能放在body標籤閉合之前,並且js邏輯不能隨意書寫!
其實這樣做是有代價的,圖片資源無法被搜索引擎正常抓取!但是這個對於電商來說,重要嗎?大家都是要通過購買流量來PK的,所以不太重要。不同版本的瀏覽器下,對於資源下載都有一定優化,webkit內核的瀏覽器表現好很多,其他瀏覽器尤其是老版本的IE表現有時候讓人搞不明。
以考拉網為案例,說明電商網站喜歡將js放在底部,其緣由其實是因為業務優化需要,但並不是每一家都會這麼做,典型的代表例如Tmall、VIP等,為什麼有還是有不少電商是不放在底部的呢?
其實很多時候還是要看業務需要,以及和一開始的技術選型有關,這個真不是一時半會可以說得清楚的,這裡就不展開了。
-----編輯於2015-09-13 09:12:23-----
(三)逆向分析考拉網的前端架構方案這裡分享的是根據別人將js放在底部,並且只能看到一大堆壓縮優化後的代碼,如何去學習別人前端架構設計的一種方法或途徑。
當然,我經常這麼做,收穫頗大,有時候想偷窺別人的源代碼,這樣才是最接近真實情況的,有時候確實也能瞎貓撞上死耗子。不管怎樣,我覺得這種方法還是可行的,分享給大家。
還是以考拉網為例。我先把首頁html框架抽出來,梳理出大致頁面結構,然後結合js,來看看別人是怎麼做的。考拉的首頁html框架如下:&
&
&
&
&
& & &
&
& &
網易考拉海購&
&&
&
& &
&
& &
&
&
& &
&
& &
&今日限時特賣&&
&&
&
&
&下期特賣預告&&
&& &
&全球精選&&
&&
&
&
& 海淘必買 / &&
&& &
&
&
&