網站為什麼 JS 調用盡量放到網頁底部?


因為瀏覽器渲染HTML文件是從上往下渲染的。

即先執行head標籤里的內容,再執行body標籤里的,一行行渲染下去。

無論當前 JavaScript 代碼是內嵌還是在外鏈文件中,頁面的下載和渲染都必須停下來等待腳本執行完成。JavaScript 執行過程耗時越久,瀏覽器等待響應用戶輸入的時間就越長。

所以JS盡量放底部可以有一定的性能優化效果。

題外話——關於JavaScript性能優化:

除了上面這個方法,還可以通過設置script標籤的defer或async屬性、合併腳本等等方法來優化。

推薦一篇乾貨:JavaScript 的性能優化:載入和執行


還有個可訪問性的考慮:如果 & 放很早但是腳本文件載入失敗或者載入緩慢(比如用手機網路的時候),用戶會看著大片的空白頁面……


因為瀏覽器載入完 JS 文件就會去執行,這時候如果 JS 裡面有對 DOM 的操作的話,DOM 可能還未載入完,就會出錯。另外由於 JS 對頁面的展示來說沒什麼用所以應首先載入 CSS 以達到儘快顯示出頁面的目的。


這是個Meta級別的好問題!如果你想把web前端性能優化到極致,一定要認真地去了解這個原則背後的原理,而非表面的技巧。

(已完結,轉載請署名,否則保留追究的權利)

事實上,如果對web優化比較了解,只要一句話就能說清楚了。web頁面性能優化其精髓就是——將瀏覽器基本無序的資源載入請求用js有序地控制起來,包括js本身。

這個原則幾乎適用於所有web場景,只是它演變出來的具體做法千差萬別,PC端和H5端由於環境不一樣,也要有不同的玩法,而Bigpipe也是基於這個理念。但請問你能理解嗎?

如果你能理解,那麼可以不往下看,如果不能理解,建議看看,或許有收穫的。我主要以PC端幾個知名電商網站的優化為案例,來說明將js放在html不同位置都有什麼不同。

-----------

一、為什麼我會認為這是個好問題?!

在我帶過的幾個電商項目前端團隊中,就因為這個問題開過幾個不開竅的前端。

理由很簡單,也比較霸道。這幾天我們項目又在做前端性能優化,又有同事拿著這個疑惑來問我,呵呵,不會再亂開人了。

以前為此開人,可能是我沒能力用通俗易懂的文字來描述好這個問題,難免有誤傷。為了避免這樣的事情,我決定要好好把問題說清楚,就從瀏覽器渲染的原則開始!

二、是不是網頁JS調用都盡量放到網頁底部?

按知乎的原則,先要問是不是,再問為什麼,但這個"是不是"對於做前端技術的人來說,一眼就能看出個所以然。

顯然,不是的!但性能優化做得好的網站基本上是這個原則。給兩個我認為前端優化做得比較好的知名海淘類電商,大家去觀摩一下別人的做法:

-----------

網易考拉海購!

貝貝網

-----------

當然,我們不能用Bigpipe這種極端的優化技術流派來解答,或許是解答不了的,但即便是bigpipe其背後的原理也是和這個原則並沒有衝突的,大道至簡,底層的原理是一樣的,只是它盡全力地利用每一次http請求,用前端的技術手段來載入足夠多的資源。

當然了,題主應該是一個前端開發,而且應該是在碰到頁面優化需求或學習上的疑惑了,可能有人強逼這TA按照這樣的原則去做,但又不知道什麼別人為啥要求他這麼做,於是就有了這個問題!

至於情況是不是這樣的,我只是瞎猜的。但是,我帶的前端團隊裡面有不少同學存在同樣的疑惑,因為我有這樣一個規範:

不知道有多少團隊有這樣的標準化demo模板規範?這是我1年多以前做的了,在這裡——基於gulp的前端框架開發規範。大家可以去我的博客看看,權當參考。當然,現在這個規範已經有了新版本。

--------------------

三、為什麼網頁JS調用都盡量放到網頁底部?

這部分如果《高性能網站建設指南》這本書上有詳細說明的並且我也認同的,就直接截圖貼過來,這裡我只說自己的理解部分。還有,對於書本上的東西,我的態度是——盡信書,不如無書。

(一)大家的做法是不是一樣的?

ok,我先把問題分解一下。

首先這裡有個關鍵詞——盡量。《高性能網站建設指南》這本書裡面用的詞是「如果可以的話」。

也就是說,js不完全是一定要放在頁面底部的,但是你要了解清楚以下兩個問題了:

  1. 什麼是盡量的(可以的)那部分js代碼?

  2. 什麼是不盡量的(不可以的)那部分呢?

只有了解清楚這兩個問題,你才知道如何去安排js在頁面中的位置。ok,我們來看看考拉網和貝貝網的首頁源代碼。

  1. 考拉網:&&之間就一段讓IE9以下瀏覽器兼容HTML5標籤的js代碼,這是一個底層的兼容腳本,不涉及任何頁面邏輯,而它的全部頁面邏輯都是放置在腳步。

  2. 貝貝網:&&之間放置的是一些全局設置和一些統計腳本,也不涉及頁面邏輯,邏輯部分js也是放在頁面底部。

  3. 我們項目的:做法是兩者的結合,heah標籤內就的js腳本就只是定義幾個全局的命名空間和一段統計腳本,沒了,而業務邏輯js就放置頁面最底部。

整體對比來看,css樣式規劃大家都基本相同,是1個全局+1個當前,文件名上通過md5戳來解決強緩存問題,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邏輯放在腳部呢?

很顯然,這並不是海淘電商的前端架構設計者有特殊的癖好,而是業務優化的需要。但是,這個需要結合不同的架構情況來具體分析,如果涉及到需要用前端模板引擎渲染頁面的,情況就更加複雜了。

比如唯品會,大家先去觀摩一下她的首頁,是不是有很多類似這樣的代碼:

&
&