Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 久艹在线观看,老子影院伦不卡欧美,亚洲六月丁香六月婷婷蜜芽

          整合營(yíng)銷(xiāo)服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢(xún)熱線:

          Google Chrome 工程師:JavaScript 不容錯(cuò)過(guò)的八大優(yōu)化建議

          摘要】本文為 Google Chrome 團(tuán)隊(duì)的開(kāi)發(fā)項(xiàng)目工程師 Addy Osmani 在PerfMatters 2019 網(wǎng)頁(yè)性能大會(huì)發(fā)表的“JavaScript性能優(yōu)化”(https://medium.com/@addyosmani/the-cost-of-javascript-in-2018-7d8950fbb5d4)的演講,其分享了處理 JavaScript 的腳本優(yōu)化建議,大幅地減少了下載時(shí)間和執(zhí)行時(shí)間。

          視頻地址:https://youtu.be/X9eRLElSW1c(需科學(xué)上網(wǎng))

          作者 | Addy Osmani

          譯者 | 蘇本如 責(zé)編 | 屠敏

          出品 | CSDN(ID:CSDNnews)

          以下為譯文:

          在過(guò)去的幾年中,由于瀏覽器的腳本解析和編譯速度的提高,Javascript成本構(gòu)成發(fā)生了巨大的變化。到了2019年,處理Javascript的開(kāi)銷(xiāo)主要體現(xiàn)在腳本下載時(shí)間和CPU執(zhí)行時(shí)間上。

          如果瀏覽器的主線程忙于執(zhí)行Javascript腳本,則用戶(hù)交互體驗(yàn)可能會(huì)受影響,因此,優(yōu)化腳本執(zhí)行時(shí)間并消除網(wǎng)絡(luò)瓶頸,會(huì)對(duì)用戶(hù)體驗(yàn)產(chǎn)生積極的作用。

          高層級(jí)的實(shí)用指南

          這對(duì)Web開(kāi)發(fā)人員來(lái)說(shuō)意味著什么?意味著解析(Parse)和編譯(Compile)不再像我們?cè)?jīng)想象的那么慢了。所以開(kāi)發(fā)人員在優(yōu)化Javascript包時(shí),要重點(diǎn)關(guān)注以下三大方面:

          減少下載時(shí)間

          • 確保Javascript包盡可能地小,特別是對(duì)于移動(dòng)設(shè)備。較小的包可以提升下載速度、降低內(nèi)存使用量,并減少CPU開(kāi)銷(xiāo)。

          • 避免只有一個(gè)大的Javascript包;如果包大小超過(guò)50–100 KB,就將其拆分為幾個(gè)小包。(借助HTTP/2協(xié)議的多路復(fù)用機(jī)制,多個(gè)請(qǐng)求和響應(yīng)消息可以同時(shí)傳輸,從而減少額外請(qǐng)求的開(kāi)銷(xiāo)。)

          • 對(duì)于移動(dòng)設(shè)備上使用的Javascript包更要盡可能地小,一方面因?yàn)榫W(wǎng)絡(luò)帶寬的制約,另一方面需要要盡量減少內(nèi)存的使用。

          縮短執(zhí)行時(shí)間

          • 避免持續(xù)占用主線程并影響頁(yè)面響應(yīng)時(shí)間的長(zhǎng)時(shí)任務(wù),現(xiàn)在腳本下載后的執(zhí)行時(shí)間成為主要的成本開(kāi)銷(xiāo)。

          避免使用大型內(nèi)聯(lián)腳本(因?yàn)樗鼈內(nèi)匀恍枰谥骶€程上進(jìn)行解析和編譯)。

          • 建議參考一條經(jīng)驗(yàn)法則:如果一個(gè)腳本超過(guò)1KB,就不要將其內(nèi)聯(lián)(因?yàn)楫?dāng)外部腳本大小超過(guò)1KB時(shí),就會(huì)觸發(fā)代碼緩存)。

          為什么下載和執(zhí)行時(shí)間很重要?

          為什么優(yōu)化下載和執(zhí)行時(shí)間對(duì)我們很重要?因?yàn)閷?duì)于低端網(wǎng)絡(luò)而言,下載時(shí)間的影響非常之大。盡管4G(甚至5G)在全球范圍內(nèi)增長(zhǎng)迅速,但大多數(shù)人的有效連接速度仍然遠(yuǎn)遠(yuǎn)低于網(wǎng)絡(luò)的標(biāo)稱(chēng)速度。有時(shí)當(dāng)我們外出時(shí),會(huì)感覺(jué)到網(wǎng)速下降到只有3G的速度(甚至更糟)。

          JavaScript的執(zhí)行時(shí)間對(duì)于CPU較慢的低端手機(jī)也非常重要。由于CPU、GPU,和散熱限制的不同,高端和低端手機(jī)的性能差距巨大。這對(duì)JavaScript的性能影響明顯,因?yàn)樗膱?zhí)行受到CPU性能的制約。

          事實(shí)上,在Chrome之類(lèi)的瀏覽器上,JavaScript的執(zhí)行時(shí)間可以達(dá)到頁(yè)面加載總耗時(shí)的30%。下圖是一個(gè)具有典型工作負(fù)載的網(wǎng)站(Reddit.com)在一臺(tái)高端桌面PC上的頁(yè)面加載情況分析:

          V8引擎下的Javascript處理時(shí)間占整個(gè)頁(yè)面加載時(shí)間的10-30%

          對(duì)于移動(dòng)設(shè)備,與高端手機(jī)(如Pixel 3)相比,在中端手機(jī)(如Moto G4)上執(zhí)行Reddit的Javascript腳本需要3-4倍的耗時(shí),而在低端手機(jī)(價(jià)格低于100美元的Alcatel 1X)上執(zhí)行Reddit的Javascript腳本更是需要6倍以上的耗時(shí):

          Reddit的Javascript腳本在幾種不同設(shè)備(低端、中端和高端)上的執(zhí)行時(shí)間。

          注意:Reddit對(duì)于桌面和移動(dòng)網(wǎng)絡(luò)有不同的體驗(yàn),因此MacBook Pro的執(zhí)行結(jié)果無(wú)法與其他結(jié)果進(jìn)行比較。

          當(dāng)你著手優(yōu)化JavaScript的執(zhí)行時(shí)間時(shí),你需要留意可能長(zhǎng)時(shí)間獨(dú)占界面線程(UI Thread)的長(zhǎng)時(shí)任務(wù)。即使頁(yè)面看起來(lái)已經(jīng)加載完成,這些長(zhǎng)時(shí)任務(wù)也會(huì)拖累關(guān)鍵任務(wù)的執(zhí)行。把長(zhǎng)時(shí)任務(wù)分解成較小的任務(wù)。通過(guò)拆分代碼并確定加載順序,你可以更快地實(shí)現(xiàn)頁(yè)面交互,并有望降低輸入延遲。

          獨(dú)占主線程的長(zhǎng)時(shí)任務(wù)應(yīng)該拆分。

          V8引擎如何提高Javascript解析/編譯速度?

          自Chrome 版本60以來(lái),V8引擎的原始JS的解析速度增加了2倍。與此同時(shí),Chrome還做了其他工作一些工作使得解析和編譯工作并行化,這使得這部分的成本開(kāi)銷(xiāo)對(duì)用戶(hù)體驗(yàn)的影響變得不是那么顯著和關(guān)鍵了。

          V8引擎通過(guò)將解析和編譯工作轉(zhuǎn)到worker線程上,使得主線程上的解析和編譯工作量平均減少了40%。例如,F(xiàn)acebook降低了46%,Pinterest降低62%,而最大的改進(jìn)是是YouTube ,降低了81%。這是在現(xiàn)有的非主線程流解析/編譯性能改進(jìn)基礎(chǔ)上的進(jìn)一步提升。

          不同版本的V8引擎的解析時(shí)間對(duì)比

          我們還可以圖示對(duì)比不同Chrome版本的不同V8引擎對(duì)CPU處理時(shí)間的影響。可以看出,Chrome 61解析Facebook的JS腳本所花費(fèi)的時(shí)間,可以供Chrome 75解析同樣的Facebook的JS腳本,和6個(gè)Twitter的JS腳本了。

          Chrome 61解析Facebook的JS腳本所花費(fèi)的時(shí)間,可以供Chrome 75解析完成同樣的Facebook的JS腳本,和6個(gè)Twitter的JS腳本了。

          讓我們深入研究一下這些改進(jìn)是如何實(shí)現(xiàn)的。總的來(lái)說(shuō),腳本資源可以在worker線程上進(jìn)行流式解析和編譯,這意味著:

          • V8引擎可以在不阻塞主線程的情況下解析和編譯JavaScript。

          • 當(dāng)整個(gè)HTML解析器遇到<script>標(biāo)記時(shí),就開(kāi)始流式處理。遇到阻塞解析器(parse-blocking)的腳本時(shí),HTML解析器就放棄,而對(duì)于異步腳本則繼續(xù)處理。

          • 在大多數(shù)網(wǎng)絡(luò)連接速度下,V8引擎的解析速度都比下載速度快,因此在最后一個(gè)腳本字節(jié)被下載后幾毫秒的時(shí)間內(nèi),V8引擎就能完成解析+編譯工作。

          具體來(lái)說(shuō),很多老版本的Chrome在開(kāi)始腳本解析之前,需要將腳本下載完成,這是一種簡(jiǎn)單的方法,但它沒(méi)有充分利用CPU的能力。而從版本41到68,Chrome在下載一開(kāi)始時(shí)就立即在單獨(dú)的線程上解析異步和延遲腳本。

          JS腳本以多個(gè)塊下載。V8引擎看到大于30KB的腳本被下載后就會(huì)啟動(dòng)腳本流解析工作。

          Chrome 71采用了基于任務(wù)(task-based)的設(shè)置方案。調(diào)度器可以一次解析多個(gè)異步/延遲腳本,這一改進(jìn)使得主線程解析時(shí)間縮短了約20%,真實(shí)網(wǎng)站上的TTI/FID整體提高了大約2%。

          Chrome 71采用了基于任務(wù)(task-based)的設(shè)置,調(diào)度器可以一次解析多個(gè)異步/延遲腳本

          Chrome 72開(kāi)始采用流式處理作為主要的解析方式,現(xiàn)在常規(guī)的同步腳本(內(nèi)聯(lián)腳本除外)也可以采用這種解析方式。如果主線程需要,我們也可以繼續(xù)采用基于任務(wù)的解析,從而減少不必要地重復(fù)工作。

          舊版的Chrome支持流式解析和編譯,其中來(lái)自網(wǎng)絡(luò)的腳本源數(shù)據(jù)必須先到達(dá)Chrome主線程后,再轉(zhuǎn)發(fā)給流解析器解析。

          這通常會(huì)導(dǎo)致這樣的情況:腳本數(shù)據(jù)已經(jīng)從網(wǎng)絡(luò)上下載完成,但由于主線程上的其他任務(wù)(如HTML解析、排版或者JavaScript執(zhí)行),阻塞了腳本數(shù)據(jù)的轉(zhuǎn)發(fā),因此流解析器(streaming parser)不得不空等。

          現(xiàn)在我們正嘗試在預(yù)加載時(shí)開(kāi)始解析,以前主線程反彈會(huì)阻礙這種操作。

          Leszek Swirski 在 BlinkOn 10 上的演講介紹了相關(guān)細(xì)節(jié):https://youtu.be/D1UJgiG4_NI(需科學(xué)上網(wǎng))

          這些改變?nèi)绾畏从车紻evTools中?

          除上述之外,DevTools中還存在一個(gè)問(wèn)題,它以表明它會(huì)獨(dú)占 CPU(完全阻塞)的方式渲染整個(gè)解析器任務(wù)。但是,不管解析器是否需要數(shù)據(jù)(數(shù)據(jù)需要通過(guò)主線程)都會(huì)阻塞。當(dāng)我我們從單個(gè)流線程轉(zhuǎn)向多個(gè)流傳輸任務(wù)時(shí),這個(gè)問(wèn)題變得非常明顯。下面是你在Chrome 69中看到的情況:

          DevTools以表明它會(huì)獨(dú)占CPU(完全阻塞)的方式渲染整個(gè)解析器任務(wù)

          如上圖示,“解析腳本”任務(wù)需要1.08秒。但是解析JavaScript其實(shí)并沒(méi)有那么慢!大部分時(shí)間除了等待數(shù)據(jù)通過(guò)主線程之外什么都做不了。

          而在Chrome 76中顯示的內(nèi)容就不一樣了:

          在Chrome 76中,解析工作被分解為多個(gè)較小的流任務(wù)。

          一般來(lái)說(shuō),DevTools性能窗格非常適合從宏觀層面分析你的頁(yè)面。對(duì)于更具體的V8度量指標(biāo),如Javascript解析和編譯時(shí)間,我們建議使用帶有運(yùn)行時(shí)調(diào)用統(tǒng)計(jì)(RCS)的Chrome跟蹤工具。在RCS結(jié)果中,Parse-Background和Compile-Background會(huì)告訴你在主線程外解析和編譯Javascript花費(fèi)了多少時(shí)間,而Parse和Compile是針對(duì)主線程的度量指標(biāo)。

          這些改變對(duì)現(xiàn)實(shí)應(yīng)用的影響是什么?

          讓我們來(lái)看一些真實(shí)網(wǎng)站的示例,來(lái)了解腳本流(script streaming)是如何工作的。

          主線程和worker線程在MacBook Pro上解析和編譯Reddit網(wǎng)站的JS所花費(fèi)的時(shí)間對(duì)比

          Reddit.com網(wǎng)站有幾個(gè)超過(guò)100KB的JS包,它們包裝在外部函數(shù)中,導(dǎo)致在主線程上需要進(jìn)行大量的延遲編譯(lazy compilation)。如上圖所示,主線程耗時(shí)才是真正關(guān)鍵的,因?yàn)橹骶€程持續(xù)繁忙會(huì)嚴(yán)重影響交互體驗(yàn)。Reddit的大部分時(shí)間花在了主線程上,而worker線程或后臺(tái)線程的使用率很低。

          可以將一些較大的JS包拆分為幾個(gè)不需要包裝的小包(例如每個(gè)包50 KB),以最大限度地實(shí)現(xiàn)并行化,這樣每個(gè)包都可以單獨(dú)進(jìn)行流解析和編譯,并在載入期間減少主線程的解析/編譯時(shí)間。

          主線程和worker線程在MacBook Pro上解析和編譯Facebook網(wǎng)站的JS所花費(fèi)的時(shí)間對(duì)比

          我們?cè)倏纯聪駀acebook.com這樣的網(wǎng)站的情況。Facebook使用了大約292個(gè)請(qǐng)求,加載了大約6MB的壓縮JS腳本,其中一些是異步的,一些是預(yù)加載的,還有一些是低優(yōu)先級(jí)的。它們的許多腳本都非常小,粒度也不大,這有助于后臺(tái)/workers線程上的整體并行化,因?yàn)檫@些較小的腳本可以同時(shí)進(jìn)行流解析/編譯。

          值得注意地是,像Facebook或Gmail這樣老牌的應(yīng)用程序的桌面版本上有這么多的腳本可能是合理的。但是你的網(wǎng)站可能和Facebook不一樣。不管怎樣,盡可能地簡(jiǎn)化你的JS包,不必要的就不要裝載了。

          盡管大多數(shù)JavaScript解析和編譯工作都可以在后臺(tái)線程上以流式方式進(jìn)行,但仍有一些工作必須在主線程上進(jìn)行。而當(dāng)主線程繁忙時(shí),頁(yè)面就無(wú)法響應(yīng)用戶(hù)輸入了。所以要密切關(guān)注下載和執(zhí)行代碼對(duì)用戶(hù)體驗(yàn)的影響。

          注意:目前并不是所有的Javascript引擎和瀏覽器都實(shí)現(xiàn)了腳本流(script streaming)式加載優(yōu)化。但是我們?nèi)匀幌嘈牛疚牡恼w指導(dǎo)會(huì)幫助大家全面地提升用戶(hù)體驗(yàn)。

          解析JSON的開(kāi)銷(xiāo)

          JSON語(yǔ)法比JavaScript語(yǔ)法簡(jiǎn)單很多,所以JSON的解析效率要比Javascript高得多。基于這一點(diǎn),Web應(yīng)用程序可以提供類(lèi)似于JSON的大型配置對(duì)象文本,而不是將數(shù)據(jù)作為Javascript對(duì)象文本進(jìn)行內(nèi)聯(lián),這樣可以大大提高Web應(yīng)用程序的加載性能。如下所示:

          const data = { foo: 42, bar: 1337 }; // 

          ……它可以用 JSON 字符串形式表示,然后在運(yùn)行時(shí)進(jìn)行 JSON 解析。如下所示:

          const data = JSON.parse('{"foo":42,"bar":1337}'); // 

          只要JSON字符串只計(jì)算一次,那么相比Javascript對(duì)象文本, JSON.parse方法就要快得多,冷加載時(shí)尤其明顯。

          在為大量數(shù)據(jù)使用普通對(duì)象文本時(shí)還有一個(gè)額外的風(fēng)險(xiǎn):它們可能會(huì)被解析兩次!

          1. 第一次是文本預(yù)解析時(shí)。

          2. 第二次是文本延遲解析時(shí)。

          第一次解析是必須的,可以將對(duì)象文本放在頂層或PIFE中來(lái)避免第二次解析。

          重復(fù)訪問(wèn)時(shí)的解析/編譯情況如何?

          V8引擎的(字節(jié))代碼緩存優(yōu)化可以幫助改善重復(fù)訪問(wèn)時(shí)的體驗(yàn)。當(dāng)?shù)谝淮握?qǐng)求腳本時(shí),Chrome會(huì)下載腳本并將其交給V8引擎進(jìn)行編譯。同時(shí)將文件存儲(chǔ)在瀏覽器的磁盤(pán)緩存中。當(dāng)?shù)诙握?qǐng)求JS文件時(shí),Chrome會(huì)從瀏覽器緩存中獲取該文件,并再次將其交給V8引擎進(jìn)行編譯。然而,這次編譯的代碼會(huì)被序列化,并作為元數(shù)據(jù)附加到緩存的腳本文件中。

          V8引擎的代碼緩存示意圖

          第三次請(qǐng)求腳本時(shí),Chrome從緩存中獲取腳本文件和文件的元數(shù)據(jù),并將兩者都交給V8引擎。V8引擎會(huì)反序列化元數(shù)據(jù)來(lái)跳過(guò)編譯步驟。如果前兩次訪問(wèn)間隔小于72小時(shí)內(nèi),代碼緩存就會(huì)啟動(dòng)。如果采用service worker來(lái)緩存腳本,那么chrome也會(huì)主動(dòng)啟動(dòng)代碼緩存。詳細(xì)信息可以參閱 web 開(kāi)發(fā)者的代碼緩存指南。

          總結(jié)

          到了2019年。腳本下載和執(zhí)行的時(shí)間開(kāi)銷(xiāo)已經(jīng)變成加載腳本的主要瓶頸。所以你應(yīng)該為你的首屏內(nèi)容準(zhǔn)備一個(gè)較小的同步(內(nèi)聯(lián))腳本包,其余部分則使用一個(gè)或多個(gè)延遲腳本,并且把較大的包拆分成許多小包來(lái)按需加載。這樣一來(lái)就能充分利用 V8 引擎的并行化能力。

          在移動(dòng)設(shè)備上,由于網(wǎng)絡(luò)、內(nèi)存消耗和CPU執(zhí)行時(shí)間的制約,你需要盡可能地減少腳本的數(shù)量,平衡延遲和緩存設(shè)置,盡可能地讓解析和編譯工作在主線程外執(zhí)行。

          原文:https://v8.dev/blog/cost-of-javascript-2019

          本文為 CSDN 翻譯,轉(zhuǎn)載請(qǐng)注明來(lái)源出處。

          【End】

          、廣告代碼分析
          很多第三方地廣告系統(tǒng)都是使用document.write來(lái)加載廣告,如下面地一個(gè)javascript地廣告鏈接.

          代碼如下:

          者 | SHERlocked93

          責(zé)編 | 胡巍巍

          大多數(shù)設(shè)備的刷新頻率是60Hz,也就說(shuō)是瀏覽器對(duì)每一幀畫(huà)面的渲染工作要在16ms內(nèi)完成,超出這個(gè)時(shí)間,頁(yè)面的渲染就會(huì)出現(xiàn)卡頓現(xiàn)象,影響用戶(hù)體驗(yàn)。前端的用戶(hù)體驗(yàn)給了前端直觀的印象,因此對(duì)B/S架構(gòu)的開(kāi)發(fā)人員來(lái)說(shuō),熟悉瀏覽器的內(nèi)部執(zhí)行原理顯得尤為重要。

          瀏覽器主要組成與瀏覽器線程

          1.1 瀏覽器組件

          瀏覽器大體上由以下幾個(gè)組件組成,各個(gè)瀏覽器可能有一點(diǎn)不同。

          • 界面控件 – 包括地址欄,前進(jìn)后退,書(shū)簽菜單等窗口上除了網(wǎng)頁(yè)顯示區(qū)域以外的部分

          • 瀏覽器引擎 – 查詢(xún)與操作渲染引擎的接口

          • 渲染引擎 – 負(fù)責(zé)顯示請(qǐng)求的內(nèi)容。比如請(qǐng)求到HTML, 它會(huì)負(fù)責(zé)解析HTML、CSS并將結(jié)果顯示到窗口中

          • 網(wǎng)絡(luò) – 用于網(wǎng)絡(luò)請(qǐng)求, 如HTTP請(qǐng)求。它包括平臺(tái)無(wú)關(guān)的接口和各平臺(tái)獨(dú)立的實(shí)現(xiàn)

          • UI后端 – 繪制基礎(chǔ)元件,如組合框與窗口。它提供平臺(tái)無(wú)關(guān)的接口,內(nèi)部使用操作系統(tǒng)的相應(yīng)實(shí)現(xiàn)

          • JS解釋器 - 用于解析執(zhí)行JavaScript代碼

          • 數(shù)據(jù)存儲(chǔ)持久層 - 瀏覽器需要把所有數(shù)據(jù)存到硬盤(pán)上,如cookies。新的HTML5規(guī)范規(guī)定了一個(gè)完整(雖然輕量級(jí))的瀏覽器中的數(shù)據(jù)庫(kù) web database

          注意:chrome瀏覽器與其他瀏覽器不同,chrome使用多個(gè)渲染引擎實(shí)例,每個(gè)Tab頁(yè)一個(gè),即每個(gè)Tab都是一個(gè)獨(dú)立進(jìn)程。

          1.2 瀏覽器中的進(jìn)程與線程

          Chrome瀏覽器使用多個(gè)進(jìn)程來(lái)隔離不同的網(wǎng)頁(yè),在Chrome中打開(kāi)一個(gè)網(wǎng)頁(yè)相當(dāng)于起了一個(gè)進(jìn)程,每個(gè)tab網(wǎng)頁(yè)都有由其獨(dú)立的渲染引擎實(shí)例。因?yàn)槿绻嵌噙M(jìn)程的話,如果瀏覽器中的一個(gè)tab網(wǎng)頁(yè)崩潰,將會(huì)導(dǎo)致其他被打開(kāi)的網(wǎng)頁(yè)應(yīng)用。另外相對(duì)于線程,進(jìn)程之間是不共享資源和地址空間的,所以不會(huì)存在太多的安全問(wèn)題,而由于多個(gè)線程共享著相同的地址空間和資源,所以會(huì)存在線程之間有可能會(huì)惡意修改或者獲取非授權(quán)數(shù)據(jù)等復(fù)雜的安全問(wèn)題。

          在內(nèi)核控制下各線程相互配合以保持同步,一個(gè)瀏覽器通常由以下常駐線程組成:

          1. GUI 渲染線程

          GUI渲染線程負(fù)責(zé)渲染瀏覽器界面HTML元素,當(dāng)界面需要重繪(Repaint)或由于某種操作引發(fā)回流(reflow)時(shí),該線程就會(huì)執(zhí)行。在Javascript引擎運(yùn)行腳本期間,GUI渲染線程都是處于掛起狀態(tài)的,也就是說(shuō)被凍結(jié)了.

          2. JavaScript引擎線程

          JS為處理頁(yè)面中用戶(hù)的交互,以及操作DOM樹(shù)、CSS樣式樹(shù)來(lái)給用戶(hù)呈現(xiàn)一份動(dòng)態(tài)而豐富的交互體驗(yàn)和服務(wù)器邏輯的交互處理。如果JS是多線程的方式來(lái)操作這些UI DOM,則可能出現(xiàn)UI操作的沖突;如果JS是多線程的話,在多線程的交互下,處于UI中的DOM節(jié)點(diǎn)就可能成為一個(gè)臨界資源,假設(shè)存在兩個(gè)線程同時(shí)操作一個(gè)DOM,一個(gè)負(fù)責(zé)修改一個(gè)負(fù)責(zé)刪除,那么這個(gè)時(shí)候就需要瀏覽器來(lái)裁決如何生效哪個(gè)線程的執(zhí)行結(jié)果,當(dāng)然我們可以通過(guò)鎖來(lái)解決上面的問(wèn)題。但為了避免因?yàn)橐肓随i而帶來(lái)更大的復(fù)雜性,JS在最初就選擇了單線程執(zhí)行。

          GUI渲染線程與JS引擎線程互斥的,是由于JavaScript是可操縱DOM的,如果在修改這些元素屬性同時(shí)渲染界面(即JavaScript線程和UI線程同時(shí)運(yùn)行),那么渲染線程前后獲得的元素?cái)?shù)據(jù)就可能不一致。當(dāng)JavaScript引擎執(zhí)行時(shí)GUI線程會(huì)被掛起,GUI更新會(huì)被保存在一個(gè)隊(duì)列中等到引擎線程空閑時(shí)立即被執(zhí)行。由于GUI渲染線程與JS執(zhí)行線程是互斥的關(guān)系,當(dāng)瀏覽器在執(zhí)行JS程序的時(shí)候,GUI渲染線程會(huì)被保存在一個(gè)隊(duì)列中,直到JS程序執(zhí)行完成,才會(huì)接著執(zhí)行。因此如果JS執(zhí)行的時(shí)間過(guò)長(zhǎng),這樣就會(huì)造成頁(yè)面的渲染不連貫,導(dǎo)致頁(yè)面渲染加載阻塞的感覺(jué)。

          3. 定時(shí)觸發(fā)器線程

          瀏覽器定時(shí)計(jì)數(shù)器并不是由JS引擎計(jì)數(shù)的, 因?yàn)镴S引擎是單線程的, 如果處于阻塞線程狀態(tài)就會(huì)影響記計(jì)時(shí)的準(zhǔn)確, 因此通過(guò)單獨(dú)線程來(lái)計(jì)時(shí)并觸發(fā)定時(shí)是更為合理的方案。

          4. 事件觸發(fā)線程

          當(dāng)一個(gè)事件被觸發(fā)時(shí)該線程會(huì)把事件添加到待處理隊(duì)列的隊(duì)尾,等待JS引擎的處理。這些事件可以是當(dāng)前執(zhí)行的代碼塊如定時(shí)任務(wù)、也可來(lái)自瀏覽器內(nèi)核的其他線程如鼠標(biāo)點(diǎn)擊、AJAX異步請(qǐng)求等,但由于JS的單線程關(guān)系所有這些事件都得排隊(duì)等待JS引擎處理。

          5. 異步http請(qǐng)求線程

          在XMLHttpRequest在連接后是通過(guò)瀏覽器新開(kāi)一個(gè)線程請(qǐng)求,將檢測(cè)到狀態(tài)變更時(shí),如果設(shè)置有回調(diào)函數(shù),異步線程就產(chǎn)生狀態(tài)變更事件放到JS引擎的處理隊(duì)列中等待處理。

          渲染過(guò)程

          2.1 渲染流程

          用戶(hù)請(qǐng)求的HTML文本(text/html)通過(guò)瀏覽器的網(wǎng)絡(luò)層到達(dá)渲染引擎后,渲染工作開(kāi)始。每次通常渲染不會(huì)超過(guò)8K的數(shù)據(jù)塊,其中基礎(chǔ)的渲染流程圖:

          webkit引擎渲染的詳細(xì)流程,其他引擎渲染流程稍有不同:

          渲染流程有四個(gè)主要步驟:

          1. 解析HTML生成DOM樹(shù) - 渲染引擎首先解析HTML文檔,生成DOM樹(shù)

          2. 構(gòu)建Render樹(shù) - 接下來(lái)不管是內(nèi)聯(lián)式,外聯(lián)式還是嵌入式引入的CSS樣式會(huì)被解析生成CSSOM樹(shù),根據(jù)DOM樹(shù)與CSSOM樹(shù)生成另外一棵用于渲染的樹(shù)-渲染樹(shù)(Render tree),

          3. 布局Render樹(shù) - 然后對(duì)渲染樹(shù)的每個(gè)節(jié)點(diǎn)進(jìn)行布局處理,確定其在屏幕上的顯示位置

          4. 繪制Render樹(shù) - 最后遍歷渲染樹(shù)并用UI后端層將每一個(gè)節(jié)點(diǎn)繪制出來(lái)

          以上步驟是一個(gè)漸進(jìn)的過(guò)程,為了提高用戶(hù)體驗(yàn),渲染引擎試圖盡可能快的把結(jié)果顯示給最終用戶(hù)。它不會(huì)等到所有HTML都被解析完才創(chuàng)建并布局渲染樹(shù)。它會(huì)在從網(wǎng)絡(luò)層獲取文檔內(nèi)容的同時(shí)把已經(jīng)接收到的局部?jī)?nèi)容先展示出來(lái)。

          2.2 渲染細(xì)節(jié)

          1. 生成DOM樹(shù)

          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)。DOM樹(shù)的根節(jié)點(diǎn)就是document對(duì)象。

          DOM樹(shù)的生成過(guò)程中可能會(huì)被CSS和JS的加載執(zhí)行阻塞,具體可以參見(jiàn)下一章。當(dāng)HTML文檔解析過(guò)程完畢后,瀏覽器繼續(xù)進(jìn)行標(biāo)記為deferred模式的腳本加載,然后就是整個(gè)解析過(guò)程的實(shí)際結(jié)束觸發(fā)DOMContentLoaded事件,并在async文檔文檔執(zhí)行完之后觸發(fā)load事件。

          2. 生成Render樹(shù)

          生成DOM樹(shù)的同時(shí)會(huì)生成樣式結(jié)構(gòu)體CSSOM(CSS Object Model)Tree,再根據(jù)CSSOM和DOM樹(shù)構(gòu)造渲染樹(shù)Render Tree,渲染樹(shù)包含帶有顏色,尺寸等顯示屬性的矩形,這些矩形的順序與顯示順序基本一致。從MVC的角度來(lái)說(shuō),可以將Render樹(shù)看成是V,DOM樹(shù)與CSSOM樹(shù)看成是M,C則是具體的調(diào)度者,比HTMLDocumentParser等。

          可以這么說(shuō),沒(méi)有DOM樹(shù)就沒(méi)有Render樹(shù),但是它們之間不是簡(jiǎn)單的一對(duì)一的關(guān)系。Render樹(shù)是用于顯示,那不可見(jiàn)的元素當(dāng)然不會(huì)在這棵樹(shù)中出現(xiàn)了,譬如 <head>。除此之外,display等于none的也不會(huì)被顯示在這棵樹(shù)里頭,但是visibility等于hidden的元素是會(huì)顯示在這棵樹(shù)里頭的。

          3. DOM樹(shù)與Render樹(shù)

          DOM對(duì)象類(lèi)型很豐富,什么head、title、div,而Render樹(shù)相對(duì)來(lái)說(shuō)就比較單一了,畢竟它的職責(zé)就是為了以后的顯示渲染用嘛。Render樹(shù)的每一個(gè)節(jié)點(diǎn)我們叫它渲染器renderer。

          一棵Render樹(shù)大概是醬紫,左邊是DOM樹(shù),右邊是Render樹(shù):

          從上圖我們可以看出,renderer與DOM元素是相對(duì)應(yīng)的,但并不是一一對(duì)應(yīng),有些DOM元素沒(méi)有對(duì)應(yīng)的renderer,而有些DOM元素卻對(duì)應(yīng)了好幾個(gè)renderer,對(duì)應(yīng)多個(gè)renderer的情況是普遍存在的,就是為了解決一個(gè)renderer描述不清楚如何顯示出來(lái)的問(wèn)題,譬如有下拉列表的select元素,我們就需要三個(gè)renderer:一個(gè)用于顯示區(qū)域,一個(gè)用于下拉列表框,還有一個(gè)用于按鈕。

          另外,renderer與DOM元素的位置也可能是不一樣的。那些添加了 float或者 position:absolute的元素,因?yàn)樗鼈兠撾x了正常的文檔流,構(gòu)造Render樹(shù)的時(shí)候會(huì)針對(duì)它們實(shí)際的位置進(jìn)行構(gòu)造。

          4. 布局與繪制

          上面確定了renderer的樣式規(guī)則后,然后就是重要的顯示元素布局了。當(dāng)renderer構(gòu)造出來(lái)并添加到Render樹(shù)上之后,它并沒(méi)有位置跟大小信息,為它確定這些信息的過(guò)程,接下來(lái)是布局(layout)。

          瀏覽器進(jìn)行頁(yè)面布局基本過(guò)程是以瀏覽器可見(jiàn)區(qū)域?yàn)楫?huà)布,左上角為 (0,0)基礎(chǔ)坐標(biāo),從左到右,從上到下從DOM的根節(jié)點(diǎn)開(kāi)始畫(huà),首先確定顯示元素的大小跟位置,此過(guò)程是通過(guò)瀏覽器計(jì)算出來(lái)的,用戶(hù)CSS中定義的量未必就是瀏覽器實(shí)際采用的量。如果顯示元素有子元素得先去確定子元素的顯示信息。

          布局階段輸出的結(jié)果稱(chēng)為box盒模型(width,height,margin,padding,border,left,top,…),盒模型精確表示了每一個(gè)元素的位置和大小,并且所有相對(duì)度量單位此時(shí)都轉(zhuǎn)化為了絕對(duì)單位。

          在繪制(painting)階段,渲染引擎會(huì)遍歷Render樹(shù),并調(diào)用renderer的 paint 方法,將renderer的內(nèi)容顯示在屏幕上。繪制工作是使用UI后端組件完成的。

          5. 回流與重繪

          回流(reflow):當(dāng)瀏覽器發(fā)現(xiàn)某個(gè)部分發(fā)生了點(diǎn)變化影響了布局,需要倒回去重新渲染。reflow 會(huì)從 <html>這個(gè) root frame 開(kāi)始遞歸往下,依次計(jì)算所有的結(jié)點(diǎn)幾何尺寸和位置。reflow 幾乎是無(wú)法避免的。現(xiàn)在界面上流行的一些效果,比如樹(shù)狀目錄的折疊、展開(kāi)(實(shí)質(zhì)上是元素的顯示與隱藏)等,都將引起瀏覽器的 reflow。鼠標(biāo)滑過(guò)、點(diǎn)擊……只要這些行為引起了頁(yè)面上某些元素的占位面積、定位方式、邊距等屬性的變化,都會(huì)引起它內(nèi)部、周?chē)踔琳麄€(gè)頁(yè)面的重新渲染。通常我們都無(wú)法預(yù)估瀏覽器到底會(huì) reflow 哪一部分的代碼,它們都彼此相互影響著。

          重繪(repaint):改變某個(gè)元素的背景色、文字顏色、邊框顏色等等不影響它周?chē)騼?nèi)部布局的屬性時(shí),屏幕的一部分要重畫(huà),但是元素的幾何尺寸沒(méi)有變。

          每次Reflow,Repaint后瀏覽器還需要合并渲染層并輸出到屏幕上。所有的這些都會(huì)是動(dòng)畫(huà)卡頓的原因。Reflow 的成本比 Repaint 的成本高得多的多。一個(gè)結(jié)點(diǎn)的 Reflow 很有可能導(dǎo)致子結(jié)點(diǎn),甚至父點(diǎn)以及同級(jí)結(jié)點(diǎn)的 Reflow 。在一些高性能的電腦上也許還沒(méi)什么,但是如果 Reflow 發(fā)生在手機(jī)上,那么這個(gè)過(guò)程是延慢加載和耗電的。可以在csstrigger上查找某個(gè)css屬性會(huì)觸發(fā)什么事件。

          reflow與repaint的時(shí)機(jī):

          1. display:none 會(huì)觸發(fā) reflow,而 visibility:hidden 只會(huì)觸發(fā) repaint,因?yàn)闆](méi)有發(fā)生位置變化。

          2. 有些情況下,比如修改了元素的樣式,瀏覽器并不會(huì)立刻 reflow 或 repaint 一次,而是會(huì)把這樣的操作積攢一批,然后做一次 reflow,這又叫異步 reflow 或增量異步 reflow。

          3. 有些情況下,比如 resize 窗口,改變了頁(yè)面默認(rèn)的字體等。對(duì)于這些操作,瀏覽器會(huì)馬上進(jìn)行 reflow。

          關(guān)鍵渲染路徑與阻塞渲染

          在瀏覽器拿到HTML、CSS、JS等外部資源到渲染出頁(yè)面的過(guò)程,有一個(gè)重要的概念關(guān)鍵渲染路徑(Critical Rendering Path)。例如為了保障首屏內(nèi)容的最快速顯示,通常會(huì)提到一個(gè)漸進(jìn)式頁(yè)面渲染,但是為了漸進(jìn)式頁(yè)面渲染,就需要做資源的拆分,那么以什么粒度拆分、要不要拆分,不同頁(yè)面、不同場(chǎng)景策略不同。具體方案的確定既要考慮體驗(yàn)問(wèn)題,也要考慮工程問(wèn)題。了解原理可以讓我們更好的優(yōu)化關(guān)鍵渲染路徑,從而獲得更好的用戶(hù)體驗(yàn)。

          現(xiàn)代瀏覽器總是并行加載資源,例如,當(dāng) HTML 解析器(HTML Parser)被腳本阻塞時(shí),解析器雖然會(huì)停止構(gòu)建 DOM,但仍會(huì)識(shí)別該腳本后面的資源,并進(jìn)行預(yù)加載。

          同時(shí),由于下面兩點(diǎn):

          1. CSS 被視為渲染 阻塞資源 (包括JS) ,這意味著瀏覽器將不會(huì)渲染任何已處理的內(nèi)容,直至 CSSOM 構(gòu)建完畢,才會(huì)進(jìn)行下一階段。

          2. JavaScript 被認(rèn)為是解釋器阻塞資源,HTML解析會(huì)被JS阻塞,它不僅可以讀取和修改 DOM 屬性,還可以讀取和修改 CSSOM 屬性。

          存在阻塞的 CSS 資源時(shí),瀏覽器會(huì)延遲 JavaScript 的執(zhí)行和 DOM 構(gòu)建。另外:

          1. 當(dāng)瀏覽器遇到一個(gè) script 標(biāo)記時(shí),DOM 構(gòu)建將暫停,直至腳本完成執(zhí)行。

          2. JavaScript 可以查詢(xún)和修改 DOM 與 CSSOM。

          3. CSSOM 構(gòu)建時(shí),JavaScript 執(zhí)行將暫停,直至 CSSOM 就緒。

          所以,script 標(biāo)簽的位置很重要。實(shí)際使用時(shí),可以遵循下面兩個(gè)原則:

          1. CSS 優(yōu)先:引入順序上,CSS 資源先于 JavaScript 資源。

          2. JavaScript 應(yīng)盡量少影響 DOM 的構(gòu)建。

          下面來(lái)看看 CSS 與 JavaScript 是具體如何阻塞資源的。

          3.1 CSS

          <style> p { color: red; }</style>

          <link rel="stylesheet" href="index.css">

          這樣的 link 標(biāo)簽(無(wú)論是否 inline)會(huì)被視為阻塞渲染的資源,瀏覽器會(huì)優(yōu)先處理這些 CSS 資源,直至 CSSOM 構(gòu)建完畢。

          渲染樹(shù)(Render-Tree)的關(guān)鍵渲染路徑中,要求同時(shí)具有 DOM 和 CSSOM,之后才會(huì)構(gòu)建渲染樹(shù)。即,HTML 和 CSS 都是阻塞渲染的資源。HTML 顯然是必需的,因?yàn)榘ㄎ覀兿M@示的文本在內(nèi)的內(nèi)容,都在 DOM 中存放,那么可以從 CSS 上想辦法。

          最容易想到的當(dāng)然是精簡(jiǎn) CSS 并盡快提供它。除此之外,還可以用媒體類(lèi)型(media type)和媒體查詢(xún)(media query)來(lái)解除對(duì)渲染的阻塞。

          <link href="index.css" rel="stylesheet">

          <link href="print.css" rel="stylesheet" media="print">

          <link href="other.css" rel="stylesheet" media="(min-width: 30em) and (orientation: landscape)">

          第一個(gè)資源會(huì)加載并阻塞。第二個(gè)資源設(shè)置了媒體類(lèi)型,會(huì)加載但不會(huì)阻塞,print 聲明只在打印網(wǎng)頁(yè)時(shí)使用。第三個(gè)資源提供了媒體查詢(xún),會(huì)在符合條件時(shí)阻塞渲染。

          關(guān)于CSS加載的阻塞情況:

          1. css加載不會(huì)阻塞DOM樹(shù)的解析

          2. css加載會(huì)阻塞DOM樹(shù)的渲染

          3. css加載會(huì)阻塞后面js語(yǔ)句的執(zhí)行

          沒(méi)有js的理想情況下,html與css會(huì)并行解析,分別生成DOM與CSSOM,然后合并成Render Tree,進(jìn)入Rendering Pipeline;但如果有js,css加載會(huì)阻塞后面js語(yǔ)句的執(zhí)行,而(同步)js腳本執(zhí)行會(huì)阻塞其后的DOM解析(所以通常會(huì)把css放在頭部,js放在body尾)

          3.2 JavaScript

          JavaScript 的情況比 CSS 要更復(fù)雜一些。如果沒(méi)有 defer 或 async,瀏覽器會(huì)立即加載并執(zhí)行指定的腳本,“立即”指的是在渲染該 script 標(biāo)簽之下的HTML元素之前,也就是說(shuō)不等待后續(xù)載入的HTML元素,讀到就加載并執(zhí)行。觀察下面的代碼:

          <p>Do not go gentle into that good night,</p>

          <script>console.log("inline1")</script>

          <p>Old age should burn and rave at close of day;</p>

          <script src="app.js"></script>

          <p>Rage, rage against the dying of the light.</p>

          <script src="app.js"></script>

          <p>Old age should burn and rave at close of day;</p>

          <script>console.log("inline2")</script>

          <p>Rage, rage against the dying of the light.</p>

          這里的 script 標(biāo)簽會(huì)阻塞 HTML 解析,無(wú)論是不是 inline-script。上面的 P 標(biāo)簽會(huì)從上到下解析,這個(gè)過(guò)程會(huì)被兩段 JavaScript 分別打斷一次(加載、執(zhí)行)。

          解析過(guò)程中無(wú)論遇到的JavaScript是內(nèi)聯(lián)還是外鏈,只要瀏覽器遇到 script 標(biāo)記,喚醒 JavaScript解析器,就會(huì)進(jìn)行暫停 (blocked )瀏覽器解析HTML,并等到 CSSOM 構(gòu)建完畢,才去執(zhí)行js腳本。因?yàn)槟_本中可能會(huì)操作DOM元素,而如果在加載執(zhí)行腳本的時(shí)候DOM元素并沒(méi)有被解析,腳本就會(huì)因?yàn)镈OM元素沒(méi)有生成取不到響應(yīng)元素,所以實(shí)際工程中,我們常常將資源放到文檔底部。

          3.3 改變腳本加載次序defer與async

          defer 與 async 可以改變之前的那些阻塞情形,這兩個(gè)屬性都會(huì)使 script 異步加載,然而執(zhí)行的時(shí)機(jī)是不一樣的。注意 async 與 defer 屬性對(duì)于 inline-script 都是無(wú)效的,所以下面這個(gè)示例中三個(gè) script 標(biāo)簽的代碼會(huì)從上到下依次執(zhí)行。

          <script async>console.log("1")</script>

          <script defer>console.log("2")</script>

          <script>console.log("3")</script>

          上面腳本會(huì)按需輸出 1 2 3,故,下面兩節(jié)討論的內(nèi)容都是針對(duì)設(shè)置了 src 屬性的 script 標(biāo)簽。

          先放個(gè)熟悉的圖~

          藍(lán)色線代表網(wǎng)絡(luò)讀取,紅色線代表執(zhí)行時(shí)間,這倆都是針對(duì)腳本的;綠色線代表 HTML 解析。

          defer:

          <script src="app1.js" defer></script>

          <script src="app2.js" defer></script>

          <script src="app3.js" defer></script>

          defer 屬性表示延遲執(zhí)行引入 JavaScript,即 JavaScript 加載時(shí) HTML 并未停止解析,這兩個(gè)過(guò)程是并行的。整個(gè) document 解析完畢且 defer-script 也加載完成之后(這兩件事情的順序無(wú)關(guān)),會(huì)執(zhí)行所有由 defer-script 加載的 JavaScript 代碼,再觸發(fā) DOMContentLoaded(初始的 HTML 文檔被完全加載和解析完成之后觸發(fā),無(wú)需等待樣式表圖像和子框架的完成加載) 事件 。

          defer 不會(huì)改變 script 中代碼的執(zhí)行順序,示例代碼會(huì)按照 1、2、3 的順序執(zhí)行。所以,defer 與相比普通 script,有兩點(diǎn)區(qū)別:載入 JavaScript 文件時(shí)不阻塞 HTML 的解析,執(zhí)行階段被放到 HTML 標(biāo)簽解析完成之后。

          async:

          async 屬性表示異步執(zhí)行引入的 JavaScript,與 defer 的區(qū)別在于,如果已經(jīng)加載好,就會(huì)開(kāi)始執(zhí)行,無(wú)論此刻是 HTML 解析階段還是 DOMContentLoaded 觸發(fā)(HTML解析完成事件)之后。需要注意的是,這種方式加載的 JavaScript 依然會(huì)阻塞 load 事件。換句話說(shuō),async-script 可能在 DOMContentLoaded 觸發(fā)之前或之后執(zhí)行,但一定在 load 觸發(fā)之前執(zhí)行。

          從上一段也能推出,多個(gè) async-script 的執(zhí)行順序是不確定的,誰(shuí)先加載完誰(shuí)執(zhí)行。值得注意的是,向 document 動(dòng)態(tài)添加 script 標(biāo)簽時(shí),async 屬性默認(rèn)是 true。

          document.createElement:

          使用 document.createElement 創(chuàng)建的 script 默認(rèn)是異步的,示例如下。

          console.log(document.createElement("script").async); // true

          所以,通過(guò)動(dòng)態(tài)添加 script 標(biāo)簽引入 JavaScript 文件默認(rèn)是不會(huì)阻塞頁(yè)面的。如果想同步執(zhí)行,需要將 async 屬性人為設(shè)置為 false。

          如果使用 document.createElement 創(chuàng)建 link 標(biāo)簽會(huì)怎樣呢?

          const style = document.createElement("link");

          style.rel = "stylesheet";

          style.href = "index.css";

          document.head.appendChild(style); // 阻塞?

          其實(shí)這只能通過(guò)試驗(yàn)確定,已知的是,Chrome 中已經(jīng)不會(huì)阻塞渲染,F(xiàn)irefox、IE 在以前是阻塞的,現(xiàn)在會(huì)怎樣目前不太清楚。

          優(yōu)化渲染性能

          結(jié)合渲染流程,可以針對(duì)性的優(yōu)化渲染性能:

          1. 優(yōu)化JS的執(zhí)行效率

          2. 降低樣式計(jì)算的范圍和復(fù)雜度

          3. 避免大規(guī)模、復(fù)雜的布局

          4. 簡(jiǎn)化繪制的復(fù)雜度、減少繪制區(qū)域

          5. 優(yōu)先使用渲染層合并屬性、控制層數(shù)量

          6. 對(duì)用戶(hù)輸入事件的處理函數(shù)去抖動(dòng)(移動(dòng)設(shè)備)

          這里主要參考Google的瀏覽器渲染性能的基礎(chǔ)講座,想看更詳細(xì)內(nèi)容可以去瞅瞅~

          4.1 優(yōu)化JS的執(zhí)行效率

          1. 動(dòng)畫(huà)實(shí)現(xiàn)使用requestAnimationFrame

          setTimeout(callback)和setInterval(callback)無(wú)法保證callback函數(shù)的執(zhí)行時(shí)機(jī),很可能在幀結(jié)束的時(shí)候執(zhí)行,從而導(dǎo)致丟幀,如下圖:

          requestAnimationFrame(callback)可以保證callback函數(shù)在每幀動(dòng)畫(huà)開(kāi)始的時(shí)候執(zhí)行。注意:jQuery3.0.0以前版本的animate函數(shù)就是用setTimeout來(lái)實(shí)現(xiàn)動(dòng)畫(huà),可以通過(guò)jquery-requestAnimationFrame這個(gè)補(bǔ)丁來(lái)用requestAnimationFrame替代setTimeout

          2. 長(zhǎng)耗時(shí)的JS代碼放到Web Workers中執(zhí)行

          JS代碼運(yùn)行在瀏覽器的主線程上,與此同時(shí),瀏覽器的主線程還負(fù)責(zé)樣式計(jì)算、布局、繪制的工作,如果JavaScript代碼運(yùn)行時(shí)間過(guò)長(zhǎng),就會(huì)阻塞其他渲染工作,很可能會(huì)導(dǎo)致丟幀。前面提到每幀的渲染應(yīng)該在16ms內(nèi)完成,但在動(dòng)畫(huà)過(guò)程中,由于已經(jīng)被占用了不少時(shí)間,所以JavaScript代碼運(yùn)行耗時(shí)應(yīng)該控制在3-4毫秒。如果真的有特別耗時(shí)且不操作DOM元素的純計(jì)算工作,可以考慮放到Web Workers中執(zhí)行。

          var dataSortWorker = new Worker("sort-worker.js");

          dataSortWorker.postMesssage(dataToSort);

          // 主線程不受Web Workers線程干擾

          dataSortWorker.addEventListener('message', function(evt) {

          var sortedData = e.data;

          // Web Workers線程執(zhí)行結(jié)束

          // ...

          });

          3. 拆分操作DOM元素的任務(wù),分別在多個(gè)frame完成

          由于Web Workers不能操作DOM元素的限制,所以只能做一些純計(jì)算的工作,對(duì)于很多需要操作DOM元素的邏輯,可以考慮分步處理,把任務(wù)分為若干個(gè)小任務(wù),每個(gè)任務(wù)都放到 requestAnimationFrame中回調(diào)執(zhí)行。

          var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);

          requestAnimationFrame(processTaskList);

          function processTaskList(taskStartTime) {

          var nextTask = taskList.pop;

          // 執(zhí)行小任務(wù)

          processTask(nextTask);

          if (taskList.length > 0) {

          requestAnimationFrame(processTaskList);

          }

          }

          4. 使用Chrome DevTools的Timeline來(lái)分析JavaScript的性能

          打開(kāi) ChromeDevTools>Timeline>JSProfile,錄制一次動(dòng)作,然后分析得到的細(xì)節(jié)信息,從而發(fā)現(xiàn)問(wèn)題并修復(fù)問(wèn)題。

          4.2 降低樣式計(jì)算的范圍和復(fù)雜度

          添加或移除一個(gè)DOM元素、修改元素屬性和樣式類(lèi)、應(yīng)用動(dòng)畫(huà)效果等操作,都會(huì)引起DOM結(jié)構(gòu)的改變,從而導(dǎo)致瀏覽器要repaint或者reflow。那么這里可以采取一些措施。

          1. 降低樣式選擇器的復(fù)雜度

          盡量保持class的簡(jiǎn)短,或者使用Web Components框架。

          .box:nth-last-child(-n+1) .title {}

          // 改善后

          .final-box-title {}

          2. 減少需要執(zhí)行樣式計(jì)算的元素個(gè)數(shù)

          由于瀏覽器的優(yōu)化,現(xiàn)代瀏覽器的樣式計(jì)算直接對(duì)目標(biāo)元素執(zhí)行,而不是對(duì)整個(gè)頁(yè)面執(zhí)行,所以我們應(yīng)該盡可能減少需要執(zhí)行樣式計(jì)算的元素的個(gè)數(shù)。

          4.3 避免大規(guī)模、復(fù)雜的布局

          布局就是計(jì)算DOM元素的大小和位置的過(guò)程,如果你的頁(yè)面中包含很多元素,那么計(jì)算這些元素的位置將耗費(fèi)很長(zhǎng)時(shí)間。布局的主要消耗在于:1. 需要布局的DOM元素的數(shù)量;2. 布局過(guò)程的復(fù)雜程度

          1. 盡可能避免觸發(fā)布局

          當(dāng)你修改了元素的屬性之后,瀏覽器將會(huì)檢查為了使這個(gè)修改生效是否需要重新計(jì)算布局以及更新渲染樹(shù),對(duì)于DOM元素的幾何屬性修改,比如width/height/left/top等,都需要重新計(jì)算布局。對(duì)于不能避免的布局,可以使用Chrome DevTools工具的Timeline查看布局的耗時(shí),以及受影響的DOM元素?cái)?shù)量。

          2. 使用flexbox替代老的布局模型

          老的布局模型以相對(duì)/絕對(duì)/浮動(dòng)的方式將元素定位到屏幕上,而Floxbox布局模型用流式布局的方式將元素定位到屏幕上。通過(guò)一個(gè)小實(shí)驗(yàn)可以看出兩種布局模型的性能差距,同樣對(duì)1300個(gè)元素布局,浮動(dòng)布局耗時(shí)14.3ms,F(xiàn)lexbox布局耗時(shí)3.5ms。IE10+支持。

          3. 避免強(qiáng)制同步布局事件的發(fā)生

          根據(jù)渲染流程,JS腳本是在layout之前執(zhí)行,但是我們可以強(qiáng)制瀏覽器在執(zhí)行JS腳本之前先執(zhí)行布局過(guò)程,這就是所謂的強(qiáng)制同步布局。

          requestAnimationFrame(logBoxHeight);

          // 先寫(xiě)后讀,觸發(fā)強(qiáng)制布局

          function logBoxHeight {

          // 更新box樣式

          box.classList.add('super-big');

          // 為了返回box的offersetHeight值

          // 瀏覽器必須先應(yīng)用屬性修改,接著執(zhí)行布局過(guò)程

          console.log(box.offsetHeight);

          }

          // 先讀后寫(xiě),避免強(qiáng)制布局

          function logBoxHeight {

          // 獲取box.offsetHeight

          console.log(box.offsetHeight);

          // 更新box樣式

          box.classList.add('super-big');

          }

          在JS腳本運(yùn)行的時(shí)候,它能獲取到的元素樣式屬性值都是上一幀畫(huà)面的,都是舊的值。因此,如果你在當(dāng)前幀獲取屬性之前又對(duì)元素節(jié)點(diǎn)有改動(dòng),那就會(huì)導(dǎo)致瀏覽器必須先應(yīng)用屬性修改,結(jié)果執(zhí)行布局過(guò)程,最后再執(zhí)行JS邏輯。

          4. 避免連續(xù)的強(qiáng)制同步布局發(fā)生

          如果連續(xù)快速的多次觸發(fā)強(qiáng)制同步布局,那么結(jié)果更糟糕。比如下面的例子,獲取box的屬性,設(shè)置到paragraphs上,由于每次設(shè)置paragraphs都會(huì)觸發(fā)樣式計(jì)算和布局過(guò)程,而下一次獲取box的屬性必須等到上一步設(shè)置結(jié)束之后才能觸發(fā)。

          function resizeWidth {

          // 會(huì)讓瀏覽器陷入'讀寫(xiě)讀寫(xiě)'循環(huán)

          for (var i = 0; i < paragraphs.length; i++)

          paragraphs[i].style.width = box.offsetWidth + 'px';

          }

          }

          // 改善后方案

          var width = box.offsetWidth;

          function resizeWidth {

          for (var i = 0; i < paragraphs.length; i++)

          paragraphs[i].style.width = width +px';

          }

          }

          注意:可以使用FastDOM來(lái)確保讀寫(xiě)操作的安全,從而幫你自動(dòng)完成讀寫(xiě)操作的批處理,還能避免意外地觸發(fā)強(qiáng)制同步布局或快速連續(xù)布局,消除大量操作DOM的時(shí)候的布局抖動(dòng)。

          4.4 簡(jiǎn)化繪制的復(fù)雜度、減少繪制區(qū)域

          Paint就是填充像素的過(guò)程,通常這個(gè)過(guò)程是整個(gè)渲染流程中耗時(shí)最長(zhǎng)的一環(huán),因此也是最需要避免發(fā)生的一環(huán)。如果Layout被觸發(fā),那么接下來(lái)元素的Paint一定會(huì)被觸發(fā)。當(dāng)然純粹改變?cè)氐姆菐缀螌傩裕部赡軙?huì)觸發(fā)Paint,比如背景、文字顏色、陰影效果等。

          1. 提升移動(dòng)或漸變?cè)氐睦L制層

          繪制并非總是在內(nèi)存中的單層畫(huà)面里完成的,實(shí)際上,瀏覽器在必要時(shí)會(huì)將一幀畫(huà)面繪制成多層畫(huà)面,然后將這若干層畫(huà)面合并成一張圖片顯示到屏幕上。這種繪制方式的好處是,使用transform來(lái)實(shí)現(xiàn)移動(dòng)效果的元素將會(huì)被正常繪制,同時(shí)不會(huì)觸發(fā)其他元素的繪制。

          2. 減少繪制區(qū)域,簡(jiǎn)化繪制的復(fù)雜度

          瀏覽器會(huì)把相鄰區(qū)域的渲染任務(wù)合并在一起進(jìn)行,所以需要對(duì)動(dòng)畫(huà)效果進(jìn)行精密設(shè)計(jì),以保證各自的繪制區(qū)域不會(huì)有太多重疊。另外可以實(shí)現(xiàn)同樣效果的不同方式,應(yīng)該采用性能更好的那種。

          3. 通過(guò)Chrome DevTools來(lái)分析繪制復(fù)雜度和時(shí)間消耗,盡可能降低這些指標(biāo)

          打開(kāi)DevTools,在彈出的面板中,選中 MoreTools>Rendering選項(xiàng)卡下的Paint flashing,這樣每當(dāng)頁(yè)面發(fā)生繪制的時(shí)候,屏幕就會(huì)閃現(xiàn)綠色的方框。通過(guò)該工具可以檢查Paint發(fā)生的區(qū)域和時(shí)機(jī)是不是可以被優(yōu)化。通過(guò)Chrome DevTools中的 Timeline>Paint選項(xiàng)可以查看更細(xì)節(jié)的Paint信息

          4.5 優(yōu)先使用渲染層合并屬性、控制層數(shù)量

          1. 使用transform/opacity實(shí)現(xiàn)動(dòng)畫(huà)效果

          使用transform/opacity實(shí)現(xiàn)動(dòng)畫(huà)效果,會(huì)跳過(guò)渲染流程的布局和繪制環(huán)節(jié),只做渲染層的合并。

          TypeFunc

          Position

          transform: translate(-px,-px)

          Scale

          transform: scale(-)

          Rotation

          transform: rotate(-deg)

          Skew

          transform: skew(X/Y)(-deg)

          Matrix

          transform: matrix(3d)(..)

          Opacity

          opacity: 0-1

          使用transform/opacity的元素必須獨(dú)占一個(gè)渲染層,所以必須提升該元素到單獨(dú)的渲染層。

          2. 提升動(dòng)畫(huà)效果中的元素

          應(yīng)用動(dòng)畫(huà)效果的元素應(yīng)該被提升到其自有的渲染層,但不要濫用。在頁(yè)面中創(chuàng)建一個(gè)新的渲染層最好的方式就是使用CSS屬性will-change,對(duì)于目前還不支持will-change屬性、但支持創(chuàng)建渲染層的瀏覽器,可以通過(guò)3D transform屬性來(lái)強(qiáng)制瀏覽器創(chuàng)建一個(gè)新的渲染層。需要注意的是,不要?jiǎng)?chuàng)建過(guò)多的渲染層,這意味著新的內(nèi)存分配和更復(fù)雜的層管理。注意,IE11,Edge17都不支持這一屬性。

          .moving-element {

          will-change: transform;

          transform: translateZ(0);

          }

          3. 管理渲染層、避免過(guò)多數(shù)量的層

          盡管提升渲染層看起來(lái)很誘人,但不能濫用,因?yàn)楦嗟匿秩緦右馕吨嗟念~外的內(nèi)存和管理資源,所以當(dāng)且僅當(dāng)需要的時(shí)候才為元素創(chuàng)建渲染層。

          * {

          will-change: transform;

          transform: translateZ(0);

          }

          4. 使用Chrome DevTools來(lái)了解頁(yè)面的渲染層情況

          開(kāi)啟 Timeline>Paint選項(xiàng),然后錄制一段時(shí)間的操作,選擇單獨(dú)的幀,看到每個(gè)幀的渲染細(xì)節(jié),在ESC彈出框有個(gè)Layers選項(xiàng),可以看到渲染層的細(xì)節(jié),有多少渲染層,為何被創(chuàng)建?

          4.6 對(duì)用戶(hù)輸入事件的處理函數(shù)去抖動(dòng)(移動(dòng)設(shè)備)

          用戶(hù)輸入事件處理函數(shù)會(huì)在運(yùn)行時(shí)阻塞幀的渲染,并且會(huì)導(dǎo)致額外的布局發(fā)生。

          1. 避免使用運(yùn)行時(shí)間過(guò)長(zhǎng)的輸入事件處理函數(shù)

          理想情況下,當(dāng)用戶(hù)和頁(yè)面交互,頁(yè)面的渲染層合并線程將接收到這個(gè)事件并移動(dòng)元素。這個(gè)響應(yīng)過(guò)程是不需要主線程參與,不會(huì)導(dǎo)致JavaScript、布局和繪制過(guò)程發(fā)生。但是如果被觸摸的元素綁定了輸入事件處理函數(shù),比如touchstart/touchmove/touchend,那么渲染層合并線程必須等待這些被綁定的處理函數(shù)執(zhí)行完畢才能執(zhí)行,也就是用戶(hù)的滾動(dòng)頁(yè)面操作被阻塞了,表現(xiàn)出的行為就是滾動(dòng)出現(xiàn)延遲或者卡頓。

          簡(jiǎn)而言之就是你必須確保用戶(hù)輸入事件綁定的任何處理函數(shù)都能夠快速的執(zhí)行完畢,以便騰出時(shí)間來(lái)讓渲染層合并線程完成他的工作。

          2. 避免在輸入事件處理函數(shù)中修改樣式屬性

          輸入事件處理函數(shù),比如scroll/touch事件的處理,都會(huì)在requestAnimationFrame之前被調(diào)用執(zhí)行。因此,如果你在上述輸入事件的處理函數(shù)中做了修改樣式屬性的操作,那么這些操作就會(huì)被瀏覽器暫存起來(lái),然后在調(diào)用requestAnimationFrame的時(shí)候,如果你在一開(kāi)始就做了讀取樣式屬性的操作,那么將會(huì)觸發(fā)瀏覽器的強(qiáng)制同步布局操作。

          3. 對(duì)滾動(dòng)事件處理函數(shù)去抖動(dòng)

          通過(guò)requestAnimationFrame可以對(duì)樣式修改操作去抖動(dòng),同時(shí)也可以使你的事件處理函數(shù)變得更輕。

          function onScroll(evt) {

          // Store the scroll value for laterz.

          lastScrollY = window.scrollY;

          // Prevent multiple rAF callbacks.

          if (scheduledAnimationFrame) {

          return;

          }

          scheduledAnimationFrame = true;

          requestAnimationFrame(readAndUpdatePage);

          }

          window.addEventListener('scroll', onScroll);

          作者簡(jiǎn)介:SHERlocked93,來(lái)自南京的前端程序員,本碩畢業(yè)于北京理工大學(xué),熱愛(ài)分享,個(gè)人公眾號(hào)「前端下午茶」,期待在這里和大家共同進(jìn)步 ~


          主站蜘蛛池模板: 国产在线精品一区二区夜色| 国产乱码一区二区三区爽爽爽| 国产无套精品一区二区| 无码人妻精品一区二区在线视频| 成人一区专区在线观看| 国产精品视频一区二区三区四| 无码人妻一区二区三区av| 日本精品高清一区二区| 视频一区视频二区在线观看| 亚洲AV无码一区二区三区久久精品 | 亚洲一区精品伊人久久伊人| 精品无人乱码一区二区三区| 精品国产一区二区三区无码| 日本一区二区在线| 一夲道无码人妻精品一区二区| 好爽毛片一区二区三区四| 色视频综合无码一区二区三区| 国产一区二区免费在线| 国产精品一区二区久久乐下载| 亚洲第一区精品观看| 无码人妻久久一区二区三区蜜桃| 国产一区二区三区在线观看免费 | 国产一区二区在线视频| 精品国产亚洲一区二区在线观看| 亚洲av无码天堂一区二区三区| 日本一区二区三区不卡视频中文字幕| 波多野结衣在线观看一区二区三区 | 国产在线精品一区二区不卡麻豆| 精品少妇一区二区三区在线| 大伊香蕉精品一区视频在线 | 国产精品自拍一区| 亚洲精品无码一区二区| 无码一区二区三区在线| 激情无码亚洲一区二区三区| 日韩一区二区三区在线| 亚洲视频在线一区二区| 色婷婷AV一区二区三区浪潮| 无码人妻精品一区二区三区66 | 少妇人妻精品一区二区| 国产一区二区三区在线观看免费| 国产高清在线精品一区二区三区 |