JS 一定要放在 Body 的最底部么

一疲吸、從一個(gè)面試題說(shuō)起
面試前端的時(shí)候我喜歡問(wèn)一些看上去是常識(shí)的問(wèn)題澄干。比如:為什么大家普遍把<script src=""></script>
這樣的代碼放在body最底部熊尉?(為了溝通效率塑悼,我會(huì)提前和對(duì)方約定所有的討論都以chrome為例)
應(yīng)聘者一般會(huì)回答:因?yàn)闉g覽器生成Dom樹(shù)的時(shí)候是一行一行讀HTML代碼的裸弦,script標(biāo)簽放在最后面就不會(huì)影響前面的頁(yè)面的渲染祟同。
我很雞賊地接著問(wèn):既然Dom樹(shù)完全生成好后頁(yè)面才能渲染出來(lái),瀏覽器又必須讀完全部HTML才能生成完整的Dom樹(shù)理疙,script標(biāo)簽不放在body底部是不是也一樣晕城?


這其實(shí)是個(gè)開(kāi)放性的問(wèn)題,里面涉及的概念的界定本身就很重要窖贤。
“頁(yè)面渲染出來(lái)了” 指的是什么砖顷?
嚴(yán)格來(lái)說(shuō)贰锁,我的最后一問(wèn)是有歧義的:我們需要統(tǒng)一一下什么叫我們經(jīng)常掛在嘴邊的“頁(yè)面渲染出來(lái)了” —— 指的是是 “首屏顯示出來(lái)了” 還是 “頁(yè)面完整地加載好了”(后面統(tǒng)稱StepC) ?如果指的是首屏顯示出來(lái)了滤蝠,那么問(wèn)題又來(lái)了:假設(shè)網(wǎng)頁(yè)首屏有圖片豌熄,這里的“首屏” 指的是 “顯示了全部圖片的首屏”(后面統(tǒng)稱StepB) 還是 “沒(méi)有圖片的首屏”(后面統(tǒng)稱StepA)。
確定清楚 “頁(yè)面渲染出來(lái)了” 指的是 StepA物咳、StepB锣险、StepC 中的哪一個(gè)是非常關(guān)鍵的(雖然至今還沒(méi)有一個(gè)應(yīng)聘者嘗試這么做過(guò)),如果 “頁(yè)面渲染出來(lái)了” 指的是 StepC览闰,那么我的最后一問(wèn)的答案是肯定的——script標(biāo)簽不放在body底部不會(huì)拖慢頁(yè)面完整地加載好的時(shí)間芯肤。
顯然,我們往往更關(guān)心首屏?xí)r間焕济,所以纷妆,如果 “頁(yè)面渲染出來(lái)了” 特指“沒(méi)有圖片的首屏”,那我的最后一問(wèn)變成了下面這樣晴弃,又該如何回答呢掩幢?
既然Dom樹(shù)完全生成好后才能顯示“沒(méi)有圖片的首屏”,瀏覽器又必須讀完全部HTML才能生成完整的Dom樹(shù)上鞠,script標(biāo)簽不放在body底部是不是也一樣际邻?

陷阱
然而上面的問(wèn)題還是存在一個(gè)陷阱——既然Dom樹(shù)完全生成好后才能顯示“沒(méi)有圖片的首屏”
這句話是帶欺騙性的,“沒(méi)有圖片的首屏”并不以“完整的Dom樹(shù)”為必要條件芍阎。也就是說(shuō):在生成Dom樹(shù)的過(guò)程中只要某些條件具備了世曾,“沒(méi)有圖片的首屏”就能顯示出來(lái)。
所以谴咸,拋開(kāi)這些歧義和陷阱轮听,我的問(wèn)題變成了:
script標(biāo)簽的位置會(huì)影響首屏?xí)r間么?

然而答案并不是那么顯而易見(jiàn)岭佳,這得從瀏覽器的渲染機(jī)制說(shuō)起血巍。(再一次說(shuō)明:本文所說(shuō)的瀏覽器都是指chrome)
二、瀏覽器的渲染機(jī)制
Google Web Fundamentals 是一個(gè)非常優(yōu)秀的文檔珊随,里面講到了跟web述寡、瀏覽器、前端的方方面面叶洞。我總結(jié)一下其中的 Ilya Grigorik 寫(xiě)的 Critical rendering path 瀏覽器渲染機(jī)制部分的內(nèi)容如下:
幾個(gè)概念
1鲫凶、DOM:Document Object Model,瀏覽器將HTML解析成樹(shù)形的數(shù)據(jù)結(jié)構(gòu)衩辟,簡(jiǎn)稱DOM螟炫。
2、CSSOMCSS Object Model惭婿,瀏覽器將CSS代碼解析成樹(shù)形的數(shù)據(jù)結(jié)構(gòu)不恭。
3叶雹、DOM 和 CSSOM 都是以 Bytes → characters → tokens → nodes → object model.
這樣的方式生成最終的數(shù)據(jù)。如下圖所示:

bVsaO
bVsaO

DOM 樹(shù)的構(gòu)建過(guò)程是一個(gè)深度遍歷過(guò)程:當(dāng)前節(jié)點(diǎn)的所有子節(jié)點(diǎn)都構(gòu)建好后才會(huì)去構(gòu)建當(dāng)前節(jié)點(diǎn)的下一個(gè)兄弟節(jié)點(diǎn)换吧。
4折晦、Render Tree:DOM 和 CSSOM 合并后生成 Render Tree,如下圖:
bVsaP
bVsaP

Render Tree 和DOM一樣沾瓦,以多叉樹(shù)的形式保存了每個(gè)節(jié)點(diǎn)的css屬性满着、節(jié)點(diǎn)本身屬性、以及節(jié)點(diǎn)的孩子節(jié)點(diǎn)贯莺。
注意:display:none
的節(jié)點(diǎn)不會(huì)被加入 Render Tree风喇,而 visibility: hidden
則會(huì),所以缕探,如果某個(gè)節(jié)點(diǎn)最開(kāi)始是不顯示的魂莫,設(shè)為 display:none
是更優(yōu)的。(具體可以看這里
瀏覽器的渲染過(guò)程
Create/Update DOM And request css/image/js:瀏覽器請(qǐng)求到HTML代碼后爹耗,在生成DOM的最開(kāi)始階段(應(yīng)該是 Bytes → characters 后)耙考,并行發(fā)起css、圖片潭兽、js的請(qǐng)求倦始,無(wú)論他們是否在HEAD里。注意:發(fā)起 js 文件的下載 request 并不需要 DOM 處理到那個(gè) script 節(jié)點(diǎn)山卦,比如:簡(jiǎn)單的正則匹配就能做到這一點(diǎn)鞋邑,雖然實(shí)際上并不一定是通過(guò)正則:)。這是很多人在理解渲染機(jī)制的時(shí)候存在的誤區(qū)账蓉。
Create/Update Render CSSOM:CSS文件下載完成枚碗,開(kāi)始構(gòu)建CSSOM
Create/Update Render Tree:所有CSS文件下載完成,CSSOM構(gòu)建結(jié)束后铸本,和 DOM 一起生成 Render Tree视译。
Layout:有了Render Tree,瀏覽器已經(jīng)能知道網(wǎng)頁(yè)中有哪些節(jié)點(diǎn)归敬、各個(gè)節(jié)點(diǎn)的CSS定義以及他們的從屬關(guān)系。下一步操作稱之為L(zhǎng)ayout鄙早,顧名思義就是計(jì)算出每個(gè)節(jié)點(diǎn)在屏幕中的位置汪茧。
Painting:Layout后,瀏覽器已經(jīng)知道了哪些節(jié)點(diǎn)要顯示(which nodes are visible)限番、每個(gè)節(jié)點(diǎn)的CSS屬性是什么(their computed styles)舱污、每個(gè)節(jié)點(diǎn)在屏幕中的位置是哪里(geometry)。就進(jìn)入了最后一步:Painting弥虐,按照算出來(lái)的規(guī)則扩灯,通過(guò)顯卡媚赖,把內(nèi)容畫(huà)到屏幕上。

以上五個(gè)步驟前3個(gè)步驟之所有使用 “Create/Update” 是因?yàn)镈OM珠插、CSSOM惧磺、Render Tree都可能在第一次Painting后又被更新多次,比如JS修改了DOM或者CSS屬性捻撑。
Layout 和 Painting 也會(huì)被重復(fù)執(zhí)行磨隘,除了DOM、CSSOM更新的原因外顾患,圖片下載完成后也需要調(diào)用Layout 和 Painting來(lái)更新網(wǎng)頁(yè)番捂。
看 Timeline,一目了然
我扒了一段有贊PC首頁(yè)的代碼到本地江解,通過(guò)Node跑起來(lái)设预。Node作為Server端,對(duì)/js/jquery.js
做了延時(shí)2s返回的處理犁河,并且把<script src="http://127.0.0.1:8080/js/jquery.js"></script>
放到導(dǎo)航欄的下面鳖枕,結(jié)果是這樣的:

bVsaO
bVsaO

bVsaO
bVsaO

bVsaO
bVsaO

bVsaO
bVsaO

從上面的Timeline我們可以看出:
首屏?xí)r間和DomContentLoad事件沒(méi)有必然的先后關(guān)系
所有CSS盡早加載是減少首屏?xí)r間的最關(guān)鍵
js的下載和執(zhí)行會(huì)阻塞Dom樹(shù)的構(gòu)建(嚴(yán)謹(jǐn)?shù)卣f(shuō)是中斷了Dom樹(shù)的更新),所以script標(biāo)簽放在首屏范圍內(nèi)的HTML代碼段里會(huì)截?cái)嗍灼恋膬?nèi)容呼股。
script標(biāo)簽放在body底部耕魄,做與不做async或者defer處理,都不會(huì)影響首屏?xí)r間彭谁,但影響DomContentLoad和load的時(shí)間吸奴,進(jìn)而影響依賴他們的代碼的執(zhí)行的開(kāi)始時(shí)間。

三缠局、問(wèn)題的答案
回到前面的問(wèn)題:
script標(biāo)簽的位置會(huì)影響首屏?xí)r間么则奥?

答案是:不影響(如果這里里的首屏指的是頁(yè)面從白板變成網(wǎng)頁(yè)畫(huà)面——也就是第一次Painting),但有可能截?cái)嗍灼恋膬?nèi)容狭园,使其只顯示上面一部分读处。
為什么說(shuō)是“有可能”呢?唱矛,如果該js下載地比css還快罚舱,或者script標(biāo)簽不在第一屏的html里,實(shí)際上是不影響的绎谦。明白這一影響邊界非常重要管闷,這樣我們?cè)诳疾祉?yè)面性能瓶頸的時(shí)候就有的放矢了。舉個(gè)例子:在網(wǎng)頁(yè)的第二屏有一個(gè)通用模塊窃肠,實(shí)際上我們是可以把它的js邏輯獨(dú)立成一個(gè)文件包个,將模塊的html和js標(biāo)簽放在一起做成獨(dú)立的模板引進(jìn)來(lái)的(如果它的js比較小或者說(shuō)因?yàn)槎嗔艘粋€(gè)文件會(huì)多占用一個(gè)TCP連接和帶寬,這實(shí)際上是另外一個(gè)話題了冤留,請(qǐng)參考我文章開(kāi)頭的聲明)碧囊。
四树灶、總結(jié)、再進(jìn)一步
所以糯而,總算弄清楚這個(gè)眾所周知的常識(shí)了天通。我們來(lái)總結(jié)一下:
如果script標(biāo)簽的位置不在首屏范圍內(nèi),不影響首屏?xí)r間
所有的script標(biāo)簽應(yīng)該放在body底部是很有道理的
但從性能最優(yōu)的角度考慮歧蒋,即使在body底部的script標(biāo)簽也會(huì)拖慢首屏出來(lái)的速度土砂,因?yàn)闉g覽器在最一開(kāi)始就會(huì)請(qǐng)求它對(duì)應(yīng)的js文件,而這谜洽,占用了有限的TCP鏈接數(shù)萝映、帶寬甚至運(yùn)行它所需要的CPU。這也是為什么script標(biāo)簽會(huì)有async或defer屬性的原因之一阐虚。

可是序臂,在復(fù)雜的實(shí)際應(yīng)用場(chǎng)景中,要貫徹這幾條結(jié)論可能會(huì)遇到問(wèn)題实束,比如:
你的頁(yè)面是分模塊來(lái)寫(xiě)的奥秆,每一個(gè)模塊都有自己的html、js甚至css咸灿,當(dāng)把這些模塊湊到一個(gè)頁(yè)面中的時(shí)候就會(huì)出現(xiàn)js自然而然地出現(xiàn)在HTML中間部分构订。你很難把script標(biāo)簽都放到底部
即使你把script標(biāo)簽都放到底部,但script標(biāo)簽的存在終究是拖慢了首屏?xí)r間避矢、DomContendLoad和loaded的時(shí)間悼瘾。如果只有一個(gè)script標(biāo)簽,我們可以加一個(gè)async审胸,但多個(gè)async的script標(biāo)簽的結(jié)果會(huì)是js文件被亂序執(zhí)行的亥宿,這顯然不是我們想要的。

我們也遇到了這樣的問(wèn)題砂沛,所以就做了一個(gè)開(kāi)源項(xiàng)目:Tiny-Loader —— A small loader that load CSS/JS in best way for page performance 簡(jiǎn)單好用烫扼。
來(lái)自:http://delai.me/code/js-and-performance/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市碍庵,隨后出現(xiàn)的幾起案子映企,更是在濱河造成了極大的恐慌,老刑警劉巖静浴,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件卑吭,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡马绝,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)挣菲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)富稻,“玉大人掷邦,你說(shuō)我怎么就攤上這事⊥指常” “怎么了抚岗?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)哪怔。 經(jīng)常有香客問(wèn)我宣蔚,道長(zhǎng),這世上最難降的妖魔是什么认境? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任胚委,我火速辦了婚禮,結(jié)果婚禮上叉信,老公的妹妹穿的比我還像新娘亩冬。我一直安慰自己,他們只是感情好硼身,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布硅急。 她就那樣靜靜地躺著,像睡著了一般佳遂。 火紅的嫁衣襯著肌膚如雪营袜。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,562評(píng)論 1 305
  • 那天丑罪,我揣著相機(jī)與錄音荚板,去河邊找鬼。 笑死巍糯,一個(gè)胖子當(dāng)著我的面吹牛啸驯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播祟峦,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼罚斗,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了宅楞?” 一聲冷哼從身側(cè)響起针姿,我...
    開(kāi)封第一講書(shū)人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎厌衙,沒(méi)想到半個(gè)月后距淫,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡婶希,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年榕暇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡彤枢,死狀恐怖狰晚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情缴啡,我是刑警寧澤壁晒,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站业栅,受9級(jí)特大地震影響秒咐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜碘裕,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一携取、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧娘汞,春花似錦歹茶、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至禽作,卻和暖如春尸昧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背旷偿。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工烹俗, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人萍程。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓幢妄,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親茫负。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蕉鸳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 問(wèn)答題47 /72 常見(jiàn)瀏覽器兼容性問(wèn)題與解決方案? 參考答案 (1)瀏覽器兼容問(wèn)題一:不同瀏覽器的標(biāo)簽?zāi)J(rèn)的外補(bǔ)...
    _Yfling閱讀 13,754評(píng)論 1 92
  • 【轉(zhuǎn)載】CSDN - 張林blog http://blog.csdn.net/XIAOZHUXMEN/articl...
    竿牘閱讀 3,488評(píng)論 1 14
  • 常見(jiàn)試題 行內(nèi)元素:會(huì)在水平方向排列忍法,不能包含塊級(jí)元素潮尝,設(shè)置width無(wú)效,height無(wú)效(可以設(shè)置line-h...
    他大舅啊閱讀 2,443評(píng)論 1 5
  • 寒冷的11月底饿序,在去親戚家串門(mén)的時(shí)候勉失,發(fā)現(xiàn)路邊有一個(gè)賣寵物的小攤,擺著小兔子原探、小倉(cāng)鼠乱凿、金魚(yú)顽素、蛇等各類小動(dòng)物。 兒子...
    小號(hào)番茄閱讀 499評(píng)論 0 0
  • 本文從熟悉微信公眾號(hào)告匠、排版技巧戈抄、欄目建設(shè)、運(yùn)營(yíng)推廣四大塊來(lái)說(shuō)后专。本文主要針對(duì)各企業(yè)的微信運(yùn)營(yíng)新手和希望自己運(yùn)營(yíng)微信公...
    牽手觀望閱讀 794評(píng)論 1 12