能每一個前端工程師都想要理解瀏覽器的工作原理。
我們希望知道從在瀏覽器地址欄中輸入 url 到頁面展現的短短幾秒內瀏覽器究竟做了什么;
我們希望了解平時常常聽說的各種代碼優化方案是究竟為什么能起到優化的作用;
我們希望更細化的了解瀏覽器的渲染流程。
瀏覽器的多進程架構
一個好的程序常常被劃分為幾個相互獨立又彼此配合的模塊,瀏覽器也是如此,以 Chrome 為例,它由多個進程組成,每個進程都有自己核心的職責,它們相互配合完成瀏覽器的整體功能,每個進程中又包含多個線程,一個進程內的多個線程也會協同工作,配合完成所在進程的職責。
對一些前端開發同學來說,進程和線程的概念可能會有些模糊,為了更好的理解瀏覽器的多進程架構,這里我們簡單討論一下進程和線程。
進程(process)和線程(thread)
進程就像是一個有邊界的生產廠間,而線程就像是廠間內的一個個員工,可以自己做自己的事情,也可以相互配合做同一件事情。
當我們啟動一個應用,計算機會創建一個進程,操作系統會為進程分配一部分內存,應用的所有狀態都會保存在這塊內存中,應用也許還會創建多個線程來輔助工作,這些線程可以共享這部分內存中的數據。如果應用關閉,進程會被終結,操作系統會釋放相關內存。更生動的示意圖如下:
一個進程還可以要求操作系統生成另一個進程來執行不同的任務,系統會為新的進程分配獨立的內存,兩個進程之間可以使用 IPC (Inter Process Communication)進行通信。很多應用都會采用這樣的設計,如果一個工作進程反應遲鈍,重啟這個進程不會影響應用其它進程的工作。
如果對進程及線程的理解還存在疑惑,可以參考下述文章:
http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
瀏覽器的架構
有了上面的知識做鋪墊,我們可以更合理的討論瀏覽器的架構了,其實如果要開發一個瀏覽器,它可以是單進程多線程的應用,也可以是使用 IPC 通信的多進程應用。
不同瀏覽器的架構模型
不同瀏覽器采用了不同的架構模式,這里并不存在標準,本文以 Chrome 為例進行說明 :
Chrome 采用多進程架構,其頂層存在一個 Browser process 用以協調瀏覽器的其它進程。
Chrome 的不同進程
具體說來,Chrome 的主要進程及其職責如下:
Browser Process:
Renderer Process:
Plugin Process:
不同進程負責的瀏覽器區域示意圖
Chrome 還為我們提供了「任務管理器」,供我們方便的查看當前瀏覽器中運行的所有進程及每個進程占用的系統資源,右鍵單擊還可以查看更多類別信息。
通過「頁面右上角的三個點點點 — 更多工具 — 任務管理器」即可打開相關面板。
Chrome 多進程架構的優缺點
優點
某一渲染進程出問題不會影響其他進程
更為安全,在系統層面上限定了不同進程的權限
缺點
由于不同進程間的內存不共享,不同進程的內存常常需要包含相同的內容。
為了節省內存,Chrome 限制了最多的進程數,最大進程數量由設備的內存和 CPU 能力決定,當達到這一限制時,新打開的 Tab 會共用之前同一個站點的渲染進程。
測試了一下在 Chrome 中打開不斷打開知乎首頁,在 Mac i5 8g 上可以啟動四十多個渲染進程,之后新打開 tab 會合并到已有的渲染進程中。
Chrome 把瀏覽器不同程序的功能看做服務,這些服務可以方便的分割為不同的進程或者合并為一個進程。以 Broswer Process 為例,如果 Chrome 運行在強大的硬件上,它會分割不同的服務到不同的進程,這樣 Chrome 整體的運行會更加穩定,但是如果 Chrome 運行在資源貧瘠的設備上,這些服務又會合并到同一個進程中運行,這樣可以節省內存,示意圖如下。
iframe 的渲染 – Site Isolation
在上面的進程圖中我們還可以看到一些進程下還存在著 Subframe,這就是 Site Isolation 機制作用的結果。
Site Isolation 機制從 Chrome 67 開始默認啟用。這種機制允許在同一個 Tab 下的跨站 iframe 使用單獨的進程來渲染,這樣會更為安全。
iframe 會采用不同的渲染進程
Site Isolation 被大家看做里程碑式的功能, 其成功實現是多年工程努力的結果。Site Isolation 不是簡單的疊加多個進程。這種機制在底層改變了 iframe 之間通信的方法,Chrome 的其它功能都需要做對應的調整,比如說 devtools 需要相應的支持,甚至 Ctrl + F 也需要支持。關于 Site Isolation 的更多內容可參考下述鏈接:
https://developers.google.com/web/updates/2018/07/site-isolation
介紹完了瀏覽器的基本架構模式,接下來我們看看一個常見的導航過程對瀏覽器來說究竟發生了什么。
導航過程發生了什么
也許大多數人使用 Chrome 最多的場景就是在地址欄輸入關鍵字進行搜索或者輸入地址導航到某個網站,我們來看看瀏覽器是怎么看待這個過程的。
我們知道瀏覽器 Tab 外的工作主要由 Browser Process 掌控,Browser Process 又對這些工作進一步劃分,使用不同線程進行處理:
瀏覽器主進程中的不同線程
回到我們的問題,當我們在瀏覽器地址欄中輸入文字,并點擊回車獲得頁面內容的過程在瀏覽器看來可以分為以下幾步:
1. 處理輸入
UI thread 需要判斷用戶輸入的是 URL 還是 query;
2. 開始導航
當用戶點擊回車鍵,UI thread 通知 network thread 獲取網頁內容,并控制 tab 上的 spinner 展現,表示正在加載中。
network thread 會執行 DNS 查詢,隨后為請求建立 TLS 連接。
UI thread 通知 Network thread 加載相關信息
如果 network thread 接收到了重定向請求頭如 301,network thread 會通知 UI thread 服務器要求重定向,之后,另外一個 URL 請求會被觸發。
3. 讀取響應
當請求響應返回的時候,network thread 會依據 Content-Type 及 MIME Type sniffing 判斷響應內容的格式。
判斷響應內容的格式
如果響應內容的格式是 HTML ,下一步將會把這些數據傳遞給 renderer process,如果是 zip 文件或者其它文件,會把相關數據傳輸給下載管理器。
Safe Browsing 檢查也會在此時觸發,如果域名或者請求內容匹配到已知的惡意站點,network thread 會展示一個警告頁。此外 CORB 檢測也會觸發確保敏感數據不會被傳遞給渲染進程。
4. 查找渲染進程
當上述所有檢查完成,network thread 確信瀏覽器可以導航到請求網頁,network thread 會通知 UI thread 數據已經準備好,UI thread 會查找到一個 renderer process 進行網頁的渲染。
收到 Network thread 返回的數據后,UI thread 查找相關的渲染進程
由于網絡請求獲取響應需要時間,這里其實還存在著一個加速方案。當 UI thread 發送 URL 請求給 network thread 時,瀏覽器其實已經知道了將要導航到那個站點。UI thread 會并行的預先查找和啟動一個渲染進程,如果一切正常,當 network thread 接收到數據時,渲染進程已經準備就緒了,但是如果遇到重定向,準備好的渲染進程也許就不可用了,這時候就需要重啟一個新的渲染進程。
5. 確認導航
進過了上述過程,數據以及渲染進程都可用了, Browser Process 會給 renderer process 發送 IPC 消息來確認導航,一旦 Browser Process 收到 renderer process 的渲染確認消息,導航過程結束,頁面加載過程開始。
此時,地址欄會更新,展示出新頁面的網頁信息。history tab 會更新,可通過返回鍵返回導航來的頁面,為了讓關閉 tab 或者窗口后便于恢復,這些信息會存放在硬盤中。
6. 額外的步驟
一旦導航被確認,renderer process 會使用相關的資源渲染頁面,下文中我們將重點介紹渲染流程。當 renderer process 渲染結束(渲染結束意味著該頁面內的所有的頁面,包括所有 iframe 都觸發了 onload 時),會發送 IPC 信號到 Browser process, UI thread 會停止展示 tab 中的 spinner。
Renderer Process 發送 IPC 消息通知 browser process 頁面已經加載完成。
當然上面的流程只是網頁首幀渲染完成,在此之后,客戶端依舊可下載額外的資源渲染出新的視圖。
在這里我們可以明確一點,所有的 JS 代碼其實都由 renderer Process 控制的,所以在你瀏覽網頁內容的過程大部分時候不會涉及到其它的進程。不過也許你也曾經監聽過 beforeunload 事件,這個事件再次涉及到 Browser Process 和 renderer Process 的交互,當當前頁面關閉時(關閉 Tab ,刷新等等),Browser Process 需要通知 renderer Process 進行相關的檢查,對相關事件進行處理。
瀏覽器進程發送 IPC 消息給渲染進程,通知要離開當前網站了
如果導航由 renderer process 觸發(比如在用戶點擊某鏈接,或者 JS 執行 window.location="http://newsite.com" ) renderer process 會首先檢查是否有 beforeunload 事件處理器,導航請求由 renderer process 傳遞給 Browser process。
如果導航到新的網站,會啟用一個新的 render process 來處理新頁面的渲染,老的進程會留下來處理類似 unload 等事件。
關于頁面的生命周期,更多內容可參考 Page Lifecycle API 。
瀏覽器進程發送 IPC 消息到新的渲染進程通知渲染新的頁面,同時通知舊的渲染進程卸載。
除了上述流程,有些頁面還擁有 Service Worker (服務工作線程),Service Worker 讓開發者對本地緩存及判斷何時從網絡上獲取信息有了更多的控制權,如果 Service Worker 被設置為從本地 cache 中加載數據,那么就沒有必要從網上獲取更多數據了。
值得注意的是 service worker 也是運行在渲染進程中的 JS 代碼,因此對于擁有 Service Worker 的頁面,上述流程有些許的不同。
當有 Service Worker 被注冊時,其作用域會被保存,當有導航時,network thread 會在注冊過的 Service Worker 的作用域中檢查相關域名,如果存在對應的 Service worker,UI thread 會找到一個 renderer process 來處理相關代碼,Service Worker 可能會從 cache 中加載數據,從而終止對網絡的請求,也可能從網上請求新的數據。
Service Worker 依據具體情形做處理。
關于 Service Worker 的更多內容可參考:
https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle
如果 Service Worker 最終決定通過網上獲取數據,Browser 進程 和 renderer 進程的交互其實會延后數據的請求時間 。Navigation Preload 是一種與 Service Worker 并行的加速加載資源的機制,服務端通過請求頭可以識別這類請求,而做出相應的處理。
更多內容可參考:
https://developers.google.com/web/updates/2017/02/navigation-preload
渲染進程是如何工作的?
渲染進程幾乎負責 Tab 內的所有事情,渲染進程的核心目的在于轉換 HTML CSS JS 為用戶可交互的 web 頁面。渲染進程中主要包含以下線程:
渲染進程包含的線程
1. 主線程 Main thread
2. 工作線程 Worker thread
3. 排版線程 Compositor thread
4. 光柵線程 Raster thread
后文我們將逐步介紹不同線程的職責,在此之前我們先看看渲染的流程。
1. 構建 DOM
當渲染進程接收到導航的確認信息,開始接受 HTML 數據時,主線程會解析文本字符串為 DOM。
渲染 html 為 DOM 的方法由 HTML Standard 定義。
2. 加載次級的資源
網頁中常常包含諸如圖片,CSS,JS 等額外的資源,這些資源需要從網絡上或者 cache 中獲取。主進程可以在構建 DOM 的過程中會逐一請求它們,為了加速 preload scanner 會同時運行,如果在 html 中存在 <img><link> 等標簽,preload scanner 會把這些請求傳遞給 Browser process 中的 network thread 進行相關資源的下載。
3.JS 的下載與執行
當遇到 <script> 標簽時,渲染進程會停止解析 HTML,而去加載,解析和執行 JS 代碼,停止解析 html 的原因在于 JS 可能會改變 DOM 的結構(使用諸如 documwnt.write()等 API)。
不過開發者其實也有多種方式來告知瀏覽器應對如何應對某個資源,比如說如果在<script> 標簽上添加了 async 或 defer 等屬性,瀏覽器會異步的加載和執行 JS 代碼,而不會阻塞渲染。更多的方法可參考 Resource Prioritization – Getting the Browser to Help You。
4. 樣式計算
僅僅渲染 DOM 還不足以獲知頁面的具體樣式,主進程還會基于 CSS 選擇器解析 CSS 獲取每一個節點的最終的計算樣式值。即使不提供任何 CSS,瀏覽器對每個元素也會有一個默認的樣式。
渲染進程主線程計算每一個元素節點的最終樣式值
5. 獲取布局
想要渲染一個完整的頁面,除了獲知每個節點的具體樣式,還需要獲知每一個節點在頁面上的位置,布局其實是找到所有元素的幾何關系的過程。其具體過程如下:
通過遍歷 DOM 及相關元素的計算樣式,主線程會構建出包含每個元素的坐標信息及盒子大小的布局樹。布局樹和 DOM 樹類似,但是其中只包含頁面可見的元素,如果一個元素設置了 display:none ,這個元素不會出現在布局樹上,偽元素雖然在 DOM 樹上不可見,但是在布局樹上是可見的。
6. 繪制各元素
即使知道了不同元素的位置及樣式信息,我們還需要知道不同元素的繪制先后順序才能正確繪制出整個頁面。在繪制階段,主線程會遍歷布局樹以創建繪制記錄。繪制記錄可以看做是記錄各元素繪制先后順序的筆記。
主線程依據布局樹構建繪制記錄
7. 合成幀
熟悉 PS 等繪圖軟件的童鞋肯定對圖層這一概念不陌生,現代 Chrome 其實利用了這一概念來組合不同的層。
復合是一種分割頁面為不同的層,并單獨柵格化,隨后組合為幀的技術。不同層的組合由 compositor 線程(合成器線程)完成。
主線程會遍歷布局樹來創建層樹(layer tree),添加了 will-change CSS 屬性的元素,會被看做單獨的一層。
主線程遍歷布局樹生成層樹
你可能會想給每一個元素都添加上 will-change,不過組合過多的層也許會比在每一幀都柵格化頁面中的某些小部分更慢。為了更合理的使用層,可參考 堅持僅合成器的屬性和管理層計數 。
一旦層樹被創建,渲染順序被確定,主線程會把這些信息通知給合成器線程,合成器線程會柵格化每一層。有的層的可以達到整個頁面的大小,因此,合成器線程將它們分成多個磁貼,并將每個磁貼發送到柵格線程,柵格線程會柵格化每一個磁貼并存儲在 GPU 顯存中。
柵格線程會柵格化每一個磁貼并存儲在 GPU 顯存中
一旦磁貼被光柵化,合成器線程會收集稱為繪制四邊形的磁貼信息以創建合成幀。
合成幀隨后會通過 IPC 消息傳遞給瀏覽器進程,由于瀏覽器的 UI 改變或者其它拓展的渲染進程也可以添加合成幀,這些合成幀會被傳遞給 GPU 用以展示在屏幕上,如果滾動發生,合成器線程會創建另一個合成幀發送給 GPU。
合成器線程會發送合成幀給 GPU 渲染
合成器的優點在于,其工作無關主線程,合成器線程不需要等待樣式計算或者 JS 執行,這就是為什么合成器相關的動畫 最流暢,如果某個動畫涉及到布局或者繪制的調整,就會涉及到主線程的重新計算,自然會慢很多。
瀏覽器對事件的處理
瀏覽器通過對不同事件的處理來滿足各種交互需求,這一部分我們一起看看從瀏覽器的視角,事件是什么,在此我們先主要考慮鼠標事件。
在瀏覽器的看來,用戶的所有手勢都是輸入,鼠標滾動,懸置,點擊等等都是。
當用戶在屏幕上觸發諸如 touch 等手勢時,首先收到手勢信息的是 Browser process, 不過 Browser process 只會感知到在哪里發生了手勢,對 tab 內內容的處理是還是由渲染進程控制的。
事件發生時,瀏覽器進程會發送事件類型及相應的坐標給渲染進程,渲染進程隨后找到事件對象并執行所有綁定在其上的相關事件處理函數。
事件從瀏覽器進程傳送給渲染進程
前文中,我們提到過合成器可以獨立于主線程之外通過合成柵格化層平滑的處理滾動。如果頁面中沒有綁定相關事件,組合器線程可以獨立于主線程創建組合幀。如果頁面綁定了相關事件處理器,主線程就不得不出來工作了。這時候合成器線程會怎么處理呢?
這里涉及到一個專業名詞「理解非快速滾動區域(non-fast scrollable region)」由于執行 JS 是主線程的工作,當頁面合成時,合成器線程會標記頁面中綁定有事件處理器的區域為 non-fast scrollable region ,如果存在這個標注,合成器線程會把發生在此處的事件發送給主線程,如果事件不是發生在這些區域,合成器線程則會直接合成新的幀而不用等到主線程的響應。
涉及 non-fast scrollable region 的事件,合成器線程會通知主線程進行相關處理。
web 開發中常用的事件處理模式是事件委托,基于事件冒泡,我們常常在最頂層綁定事件:
復制代碼
document.body.addEventListener('touchstart', event=> { if (event.target===area) { event.preventDefault(); } } );
上述做法很常見,但是如果從瀏覽器的角度看,整個頁面都成了 non-fast scrollable region 了。
這意味著即使操作的是頁面無綁定事件處理器的區域,每次輸入時,合成器線程也需要和主線程通信并等待反饋,流暢的合成器獨立處理合成幀的模式就失效了。
由于事件綁定在最頂部,整個頁面都成為了 non-fast scrollable region。
為了防止這種情況,我們可以為事件處理器傳遞 passive: true 做為參數,這樣寫就能讓瀏覽器即監聽相關事件,又讓組合器線程在等等主線程響應前構建新的組合幀。
復制代碼
document.body.addEventListener('touchstart', event=> { if (event.target===area) { event.preventDefault() } }, {passive: true} );
不過上述寫法可能又會帶來另外一個問題,假設某個區域你只想要水平滾動,使用 passive: true 可以實現平滑滾動,但是垂直方向的滾動可能會先于event.preventDefault()發生,此時可以通過 event.cancelable 來防止這種情況。
復制代碼
document.body.addEventListener('pointermove', event=> { if (event.cancelable) { event.preventDefault(); // block the native scroll /* * do what you want the application to do here */ } }, {passive: true});
也可以使用 css 屬性 touch-action 來完全消除事件處理器的影響,如:
復制代碼
#area { touch-action: pan-x; }
查找到事件對象
當組合器線程發送輸入事件給主線程時,主線程首先會進行命中測試(hit test)來查找對應的事件目標,命中測試會基于渲染過程中生成的繪制記錄( paint records )查找事件發生坐標下存在的元素。
主線程依據繪制記錄查找事件相關元素。
事件的優化
一般我們屏幕的刷新速率為 60fps,但是某些事件的觸發量會不止這個值,出于優化的目的,Chrome 會合并連續的事件 (如 wheel, mousewheel, mousemove, pointermove, touchmove ),并延遲到下一幀渲染時候執行 。
而如 keydown, keyup, mouseup, mousedown, touchstart, 和 touchend 等非連續性事件則會立即被觸發。
Chrome 會合并連續事件到下一幀觸發。
合并事件雖然能提示性能,但是如果你的應用是繪畫等,則很難繪制一條平滑的曲線了,此時可以使用 getCoalescedEvents API 來獲取組合的事件。示例代碼如下:
復制代碼
window.addEventListener('pointermove', event=> { const events=event.getCoalescedEvents(); for (let event of events) { const x=event.pageX; const y=event.pageY; // draw a line using x and y coordinates. } });
花了好久來整理上面的內容,整理的過程收獲還挺大的,也希望這篇筆記能對你有所啟發,如果有任何疑問,歡迎一起來討論。
本文經作者授權轉載,原文鏈接為:
https://zhuanlan.zhihu.com/p/47407398
參考鏈接
日,國外統計公司Statcounter公布了全球瀏覽器市場份額調查數據。無論是桌面端還是移動端,谷歌Chrome瀏覽器名列第一,大幅領先其他品牌。如果將統計范圍限制在國內的話,Chrome瀏覽器依然牢牢掌握第一的寶座,QQ瀏覽器名列第二,搜狗瀏覽器位列第四,都屬于國內比較常用的瀏覽器品牌。
不過數據終究只是數據,僅供參考。先不提Chrome,其他的國產瀏覽器大多都不夠簡潔干凈,安裝時會綁定一些奇奇怪怪的東西,還有就是安裝后,會推送各種亂七八糟的東西,比如會誘導你安裝該公司的其他服務,譬如某瀏覽器通過讓你清理垃圾向你推薦XX毒霸,XX衛士等,一不留神很容易中招。這些行為,雖說是商業化不可避免的手段,但實在讓用戶非常煩擾,不過大多數人都把這些不滿咽進肚子里了。其實我們大可不必使用這些瀏覽器,網上仍然有不少優質瀏覽器,那么今天猿仔就來扒一扒優秀,相對比較干凈的瀏覽器。
相信不少人,對于猿仔會推薦谷歌瀏覽器會很詫異:
谷歌難道不是人人知道的瀏覽器嗎?Google 瀏覽器真的好用?感覺沒有什么特別的啊,還不如國內360瀏覽器,獵豹這些人性化功能多呢?!
的確,單單從瀏覽器集成的功能上邊看,谷歌瀏覽器確實沒有其他國產軟件豐富。但是谷歌瀏覽器的優點恰恰就體現在它的高度簡潔上邊,因為這個時候的Google Chrome就像一張白紙,用戶可以通過安裝眾多強大好用的擴展程序,從而實現個性化定制瀏覽器。下面就羅列一些常見的拓展程序:
谷歌瀏覽器比較適合開發同學以及廣大愛折騰,愛自由的高配人士使用。懶人們就還是算了吧,不裝裝插件的話,用起來也挺郁悶的,而且說實話,Chrome內核太占內存了,內存稍微低一些的小伙伴,分分鐘卡死你。
【Edge瀏覽器】是微軟Win 10自帶的瀏覽器,前身就是IE瀏覽器。IE本身也是個優秀的瀏覽器,可惜微軟沒有把握機會好好發展它,也許微軟看到沒有任何超越Chrome的希望,索性在去年把【Edge瀏覽器】內核也替換成谷歌內核,你沒看錯!就是Chromium引擎。
這意味著【Edge瀏覽器】也是可以安裝谷歌插件,甚至是可以直接同步谷歌賬號,你在谷歌瀏覽器的書簽插件什么都可以直接同步過來,不需要特殊的上網方式。從這點上【Edge瀏覽器】甚至比Chrome本體還要好用,而且它還有自家的插件程序商店,拓展油猴,iDM下載工具,視頻解析等功能都不在話下。
對于程序員來說,chrome是主流瀏覽器,chrome以簡潔著稱,也極少出什么大錯,所謂的大版本更新,基本都是肉眼看不見的更新。Chrome萬年不變的界面,實在有些單調,看久了難免想換換口味,畢竟生活還是需要點小驚喜的。那么,Vivaldi瀏覽器,將會成為你第一個驚喜。Vivaldi瀏覽器采用Chrome內核,所以可以從Chrome無縫切換過去,是一款可以替代Chrome的極品瀏覽器。
動態配色
Vivaldi的界面配色并不固定,它會根據你所在的網站的配色,將瀏覽器上的部分組件顏色進行同步變換,這個是Vivaldi 瀏覽器最為驚艷的部分。比如說如果在知乎瀏覽,瀏覽器的界面就會變成知乎藍。如果是網易云音樂的話呢,則會變成姨媽紅。相信各位顏值黨會對Vivaldi的動態配色功能愛不釋手。
標簽頁分類合并
Vivaldi可以將多個相同網頁的標簽頁合并為一個大標簽頁,可以將不同站點的網站歸類,現在,只需要鼠標懸停在大標簽頁上,即可找到我們想要的網頁了。特別適合經常需要打開大量網頁的工作者。
網頁筆記
Vivaldi內置筆記功能,從側邊欄調出即可使用,支持添加圖片和附件, 非常方便,體驗比第三方筆記軟件好很多。
標簽休眠
Vivaldi雖然使用了chrome的內核,那么Vivaldi是如何處理內存占用過高的問題呢?Vivaldi貼心地增加了標簽休眠功能,你只需要右鍵暫時不需要使用的標簽頁,然后休眠后臺標簽。如果你需要再次使用,就再次點擊標簽頁即可。
用過Vivaldi瀏覽器的人都說:好用得一比!!不信你試試。
Firefox也曾經一度是 W3C 標準的引領者,風光無限,后來市場份額急劇縮小,Firefox 成了電腦極客的小眾選擇,直到全新架構的Firefox量子版本出來后,性能大幅度提升,非常值得體檢,情況才有所好轉。
Firefox有國內代理和國際兩個版本。國際版再也不用擔心安裝時捆綁其它工具的問題,也不用擔心其它瀏覽器主頁被篡改,還不用被迫接受chrome般無法修改安裝位置的霸道。
Firefox有哪些亮點呢:
Firefox在趕超chrome方面,做了很多優化和創新,雖然目前比起chrome,穩定性要差一些,但瑕不掩瑜,Firefox依舊值得大家下載使用。
360極速瀏覽器也是基于Chromium內核,內存占用比谷歌要小,出現假死的幾率更小得多。極速版的360瀏覽器,猿仔一直在用,在用戶體驗方面很不錯,很多小細節都值得稱道。
比如:快速保存圖片功能,只需要安卓ALT,用鼠標點擊任意圖片就可以保存到本地,對于猿仔來說,簡直不要太方便。
新版的360極速瀏覽器還加入了以下幾個特性:
360雖然在其它產品的用戶評價上褒貶不一,猿仔這里不作評論。但有一說一,360極速瀏覽器,各方面來說還是體驗還是很友好的,可以試試。
星愿瀏覽器的開發愿景是做出一款符合學生使用的瀏覽器。目前全職的開發人員只有兩人,所以在功能上有所取舍,但盡可能地滿足學生的個性化需求:
如果你想找一款類似谷歌瀏覽器好用,又幫你集成常見實用功能的瀏覽器,那么Cent Browser就是一個極佳的選擇。Cent Browser百分瀏覽器是國內瀏覽器研發的技術團隊百分工作室(Cent Studio)采用谷歌的開源Chromium開發者引擎系統編譯的一款附加諸多實用功能的追求速度、簡約和安全的增強版網頁瀏覽器。
Cent Browser甚至被稱為全球最好用的第三方谷歌瀏覽器編譯增強版。由于國內使用谷歌應用商店很困難,因此Cent官方提供許多用戶常用的實用功能,如鼠標手勢,廣告過濾Adblock等等十幾種插件,這個就是Cent Browser為人稱道的地方了。
為什么要了解瀏覽器原理?
當面試官問你輸入url到渲染發生了什么這種問題你不知所措?
頁面中到底能承載多少個元素,取決于什么條件?如果一個頁面在2s內打不開,你應該如何優化?
DOM是什么,javascript操作的是DOM還是html?
回流和重繪又是什么?瀏覽器架構是什么樣的?
當你能夠細化的了解整個瀏覽器工作原理的時候,你就能很好的處理這些問題
瀏覽器我們常用的有谷歌 IE Safari 火狐等等,目前開發者心中的瀏覽器只有一個,那就是谷歌瀏覽器,它的市場份額穩居第一,從未被超越
以工程師的維度,或者開發者的維度怎么看瀏覽器,它是一套標準,這套標準可以運行html、css、javascript代碼,這些內容可以通過超文本傳輸協議進行傳輸,通過瀏覽器的標準進行展現,能夠讓世界上任何使用瀏覽器的人都能夠看到網頁內容
頁面的標準有W3C,語言標準有ECMAScript,當然還有網絡標準,等各種標準由瀏覽器統一管理。這些標準共同作用,即便是不同的瀏覽器,它們的標準也是一致的。當然并不是完全一致,一些瀏覽器還是有自己的想法的,就是這些想法給前端程序員帶來了巨大的工作量,比如工程師們聞之色變的兼容IE,主要原因就是IE瀏覽器有自己的想法,搞了很多和別人不太一樣的東西,導致同樣的代碼和邏輯,在IE瀏覽器上展示就有問題
在深入研究瀏覽器架構之前要掌握的另一個概念是進程和線程。通常一個好的應用程序會把自己劃分為幾個獨立的模塊和幾個相互配合的模塊,瀏覽器本身也是這樣,谷歌瀏覽器為例,它由多個線程和多個進程組成,進程之間相互協作完成瀏覽器整體的大功能,進程又包含很多線程,一個進程內的線程也會相互配合完成這個進程的工作職責
對于前端同學來說,這些概念是模糊的,概念上講什么進程是資源分配的最小單位,線程是CPU調度的最小單位,很多同學聽了可能還是有些懵懂,我們用圖來簡單講述一下
以工廠為例,我們可以把工廠理解成進程,工人理解成線程,工人只能在工廠工作,一個工廠可以有很多工人,同一個工廠內的工人很容易交流,不同的工廠內的工人不容易交流,只是會比較費勁,一個工廠不會影響另一個工廠,但是工廠內的工人會影響這個工廠的運行
當啟動一個應用程序時,會創建一個進程或者多個進程,該程序可能會創建線程來幫助它工作。操作系統為每個進程提供了可用的內存,所有應用程序狀態都保存在內存空間中。當您關閉應用程序時,程序創建的進程也會消失,占有的內存也會被釋放
了解了進程和線程的關系之后,我們可以看一下啟動chrome瀏覽器需要占用多少進程
谷歌瀏覽器自帶了一個任務管理器,點擊瀏覽器【更多工具】→【任務管理器】
可以看到對應的瀏覽器進程
這張圖其實就表明瀏覽器其實是多進程的架構,當然這是不斷演化的結果,并不是一蹴而就的,簡單來看一下這些進程的作用
為什么每個標簽或者每個插件都要一個進程呢?其實思考一下不難理解,如果我們打開的某一個網頁無響應了,或者崩潰了,這個時候每個進程是隔離的,并不會影響我們其它的頁面,這種設計其一是保證了標簽頁的穩定性。同樣的如果某個頁面死循環不流暢,其它頁面也是感知不到的,內存泄漏也一樣,當前頁面由于內存泄漏關閉這個標簽頁之后,對應的內存資源就會被回收,變相解決了內存泄漏導致整個瀏覽器崩潰的問題
多進程還有一個好處就是安全性,因為操作系統有一種限制進程權限的方法,所以瀏覽器可以通過某些功能對某些進程采取沙箱處理,比如瀏覽器限制了渲染進程的任意文件的訪問。沙箱處理隔離了渲染進程,這樣即使在渲染進程里面執行惡意程序,惡意程序也無法突破沙箱獲取系統的權限
當然并不是多進程都是好的,也有其不足之處,比如我打開了多個百度頁面的選項卡,這個時候瀏覽器分配了多個進程,導致更多的資源占用,雖然這兩個內存中的東西完全一致
當然谷歌瀏覽器針對這一點也做了對應的優化,它的內部限制了可以啟動的進程數量,這個限制取決于你的設備的內存和cpu的功率,并不是固定死的,設備越好可啟動的進程數量就越多。當達到它所限制的數量時,它會優化打開的標簽頁,比如相同站點的標簽頁合并為同一個進程
當然多個標簽跟開啟多個瀏覽器類似,谷歌瀏覽器也在不斷優化,將瀏覽器中的各個部分作為一項服務,從多進程模型到多服務模型,可以輕松的進行進程拆分或者合并。也就是說當你的硬件性能足夠,它可以將每個服務拆分到不同的進程,當你的硬件資源有限,它會將這些服務合并到一個進程
前面說我們每個標簽頁一個進程,但是這個標簽頁當中有可能通過iframe嵌入了另一個頁面,這個時候如果共用一個進程的話可能就會帶來風險。所以谷歌瀏覽器為跨站點的iframe頁面也開啟了一個單獨的渲染進程。要考慮瀏覽器同源策略的影響,一個站點無法在未經允許的情況下訪問其它站點的數據,進程隔離是分割站點的最有效的方式
站點隔離并不是我們想象的這么簡單,它改變了iframe和頁面的交互方式,即便是多個渲染進程,當你打開devtools的時候,它們看起來還是那么完美,并且當你用ctrl+f對頁面進行搜索的時候,多個渲染進程之間的搜索工程師們也優化的你看不出絲毫破綻
了解了瀏覽器的架構,來講一個面試官最喜歡問的面試題,當瀏覽器輸入url之后發生了什么?
我們使用瀏覽器的主要目的就是為了搜索或者訪問某些網站,就讓我們從瀏覽器的角度,來看看我們是如何進行搜索或者網站的訪問的
從瀏覽器的架構中我們可以得知,我們輸入url或者搜索的這一欄是由瀏覽器進程控制的,其中瀏覽器進程下面有一些線程,比如控制搜索欄交互和展示的UI線程,當你輸入網址或者文字之后,UI線程便開始了工作
當你在地址欄輸入了內容之后,UI線程要做的操作就是需要進行辨別,判斷你輸入的是url還是搜索的字段,如果是url,則相應的轉到對應的站點。如果是搜索的字段,則通過瀏覽器中設置的使用那種搜索引擎,進行對應的站點跳轉
不論是搜索還是站點訪問,最終都會走站點訪問的邏輯,當你在地址欄輸入【你好】之后,回車,它也會變成相應的站點url
要判斷是否是URL就要知道什么是URL(「U」niform 「R」esource 「L」ocator)翻譯過來為統一資源定位符,俗稱網址
它的標準格式為:
?[協議類型]://[服務器地址]:[端口號]/[資源層級UNIX文件路徑][文件名]?[查詢]#[片段ID] ?
一個簡單的URL組成如下所示
一個完整的URL如下所示
只要輸入的內容滿足這個規則,就認為是一個有效的URL,直接跳轉到對應的網站,否則就執行搜索邏輯(本質還是跳轉到對應的URL)
拿到URL之后,是不是立刻就會發送請求?其實不是,瀏覽器還會進行額外的一些檢查。
比如安全性檢查,檢查要訪問的內容在本地是不是有緩存,緩存是否過期?一個較為完整的前置流程判斷大致如下
這張圖其實畫的有點多,但是主要是想讓大家了解在獲取資源之前,其實是有一個緩存的判斷的,否則就不會發送對應的請求
在控制臺能夠看到一些資源雖然返回了200的狀態碼,但是實際是來自緩存,并沒有從服務器獲取數據,抓包的話也是沒有對應的請求的
上面其實講的是強緩存,強緩存是有對應的過期時間的,時間是響應標頭expires控制,當然圖中還有標注cache-control這個字段,這兩個字段有啥區別呢?
expires是http1.0的產物,cache-control是1.1的產物,兩個同時存在,cache-control的優先級更高
圖中還有一個字段,是last-modified,這個主要用于協商緩存,如果強緩存生效,則直接走強緩存,不生效則走協商緩存,協商緩存會在請求頭中添加if-Modified-Since的字段,這個字段的值就是第一次請求文件的時候返回頭中的last-modified,服務收到請求后,對比服務中文件修改的時間,如果沒變化,狀態碼返回304,瀏覽器直接從緩存中獲取文件,如果有更新,則服務端會返回200狀態,并返回新的文件,最后瀏覽器再將新的響應報文和對應的緩存標識緩存起來
協商緩存還有一個字段是etag,圖中也有,它是根據文件內容是否有修改來決定是否使用緩存數據
上述的緩存有些內容可能還未涉及,但是大家先了解一個大概,像請求報文之類的我們后續會詳細說明,因為這里涉及到緩存,大家有個概念即可,我們看一下正常的請求流程是什么樣的,不走緩存的這種
如果緩存都未命中,我們就需要瀏覽器去發送請求,請求網址對應的資源,但是我們都知道,服務器的地址都是一段ip地址,但是我們明明輸入的是一個URL,URL怎么能夠知道我們訪問的是哪一個服務器呢?
這就引出了DNS的概念,DNS其實就是用于實現域名和IP相互映射的一個分布式數據庫,它可以將域名翻譯成計算機可識別的IP地址
借用網上的一張圖,來看看DNS的查詢流程是怎樣的
DNS是極其重要的一環,這個環節出了問題就無法進行后續的操作,它是客戶端訪問互聯網的關鍵所在
通過DNS解析我們已經獲取到了文件所在的服務器的IP,有了這個IP之后,我們就需要發送請求獲取對應的文件了,但是在獲取文件的第一步,首先要做的就是建立TCP連接
一個頁面的性能的影響因素首要的就是首屏渲染時間,而首屏渲染時間其中的一個影響因素就是網絡加載速度,決定性的內容就是文件的返回時間
如果你對網絡有充分的了解,那肯定知道網絡的基礎是網絡協議,網站則是基于http協議,http又是基于TCP/IP的。計算機底層是101010這種二進制數據,文件傳輸也是二進制數據,那這些數據是如何到我們的瀏覽器的?第一步就是要建立服務器與客戶端之間的連接
上面我們已經獲取到了服務端的IP地址,每個計算機都有一個獨立的IP地址,客戶端和服務端有了各自的地址之后就能夠精準傳輸了,整個過程如下圖所示
建立連接的過程就是三次握手的過程,表示整個過程要發送三次包
連接建立成功,就可以傳輸數據了
數據傳輸完成,有一個斷開的過程
斷開連接被稱為四次揮手過程,表示整個斷開過程要發送四次包,需要雙向連接和雙向關閉,不管是客戶端還是服務端,任何一方都可以發起斷開的過程
上一步已經建立好了連接,那就開始傳輸數據了。在該階段,客戶端需要對數據包進行確認操作,在接收到數據包之后,需要發送確認數據包給發送端。所以發送了一個數據包之后,在規定時間內沒有接收到客戶端端反饋的確認消息,則判斷為數據包丟失,并觸發發送端的重發機制。同樣,一個大的文件在傳輸過程中會被拆分成很多小的數據包,這些數據包到達接收端后,接收端會按照TCP頭中的序號為其排序,從而保證組成完整的數據。
通過TCP以及UDP共同作用,這個時候瀏覽器的網絡線程是能夠收到服務器的完整數據,在獲取數據的時候,我們會添加一系列的請求頭,比如我們必須指定請求方法到底是GET還是POST,或者是其它,之前我們也提到了If-Modified-Since,用于緩存,還有Cookie、User-Agent、Referer等頭信息
用這些請求頭數據去告訴服務器我們當前需要什么內容,以及告訴服務端客戶端的一些信息
當服務端同意或者拒絕給客戶端返回內容之后,客戶端都會收到一些反饋,反饋的內容除了正常我們想要的數據以外,還有response頭信息。可以看到對應的狀態代碼為200,表示成功,說明獲取到了服務端返回的對應信息。
這個域名對應的IP+端口就是圖中的遠程地址,除了狀態為200還有其它的一些狀態,比如301告知服務器正在重定向,然后網絡線程發起另一個URL請求,針對http狀態碼各個階段的含義大家可以自行了解一下
上面的圖中可以看到響應頭里面包含了一些信息或者執行了一些操作,比如Set-Cookie響應頭可以往瀏覽器里面設置一些cookie,Conetnt-Type、Cache-Control等相關
網絡線程在查看了頭部的這些字節之后,因為傳輸過程有可能會出現異常,比如丟失或者錯誤,所以在這里會完成MIME類型嗅探(也就是檢查一個字節流的內容,試圖推斷其中文件的數據格式)
上面我們在訪問百度的時候,Conetnt-Type為html類型的文件,瀏覽器檢測到如果文件類型是html的話,之后就會把數據交給渲染進程。當然這里不僅僅只有html文件類型,如果是其它文件類型,比如zip或者其它的內容,則瀏覽器會交給下載管理器進行對應資源的下載
這個時候通常也會進行瀏覽器的安全檢查,兩方面檢查
所以我們要明確的一點是,跨域是瀏覽器的安全策略,是瀏覽器攔截的,如果你用抓包工具的話,會發現數據其實已經給到我們了,當然post請求還會存在一個預檢的過程,防止抓到數據,在發正式請求之前,預檢服務端是否做了跨域的處理
當前已經準備好了對應的數據,也就是html文件。并且完成了前置的所有信息檢查,那么網絡線程就會告訴UI線程數據已經準備就緒,UI線程要做的就是找一個渲染進程用于html的渲染
但是這個過程是有優化的空間的,因為網絡線程請求數據的過程是需要時間的,所以在網絡線程發送URL的請求的時候,它已經知道當前是要訪問哪個站點,UI線程將會并行查找并啟動渲染進程,這個時候請求到數據的時候,渲染進程已經是待命的狀態,可用于直接渲染
這個時候需要瀏覽器進程跟渲染進程通過IPC進行通信,通信過程還需要傳遞數據流,方便渲染進程可以持續接收html數據,一旦渲染進程渲染完畢,便會通知瀏覽器進程當前完畢,導航階段就完成了,就開始了加載文檔的過程
這個時候,地址欄更新。安全指示器和站點設置的UI反應站點的信息,選項卡的歷史記錄會被更新,前進后退等歷史記錄逐步被更新,歷史記錄同樣也會在磁盤上存儲一份,方便進行整個歷史瀏覽的檢索
我們知道,當頁面進行加載的時候,瀏覽器UI上tab標簽頁上會有一個加載中的loading標志,一旦渲染進程完成渲染,渲染進程會將回調通過IPC發送到瀏覽器進程(onload事件完成的時候,包含所有子頁面(frame)),瀏覽器UI上loading標志消失,顯示完成狀態,但是這個結束并不代表頁面渲染就完成了,有可能還有JavaScript在加載額外的資源或者新的視圖
這個時候渲染進程便開始渲染,具體是如何渲染的我們之后詳細講述,我們再看一下在這基礎如何訪問另一個頁面
在當前標簽頁,我們進行另一個頁面訪問的時候,瀏覽器進程會重復上面的過程。但是開始的時候,瀏覽器會確認當前的站點是否關心beforeunload這個事件,如果對這個事件做了監聽,當訪問另一個網站或者刷新的時候,就會彈出下選項進行確認
window.addEventListener('beforeunload', (event)=> {
// 顯示確認對話框
event.preventDefault();
// 為了兼容處理,Chrome需要設置returnValue
event.returnValue='';
});
當然,這只是眾多生命周期中的一個節點,還有比如unload,pagehide,pageshow等等,感興趣的可以參照
https://developers.google.com/web/updates/2018/07/page-lifecycle-api
這里我們再插入一個知識點,service worker,它的作用是什么呢?其實就是服務器和瀏覽器之間的一個中間人。目的是為了攔截網站的所有請求,可以進行相應的判斷,如果一些接口可以直接使用緩存就直接返回緩存
service worker獨立于當前網頁的線程,所以執行大量的操作也不會阻塞主線程
上面的過程把html文件已經交給了渲染進程,渲染進程負責頁簽的顯示,在一個渲染進程中,主線程負責解析,編譯代碼,運行等工作,它的核心就是將HTML、CSS和JavaScript轉換成用戶可以與之交互的網頁
當然渲染進程是一個多線程架構,它主要有以下線程:GUI線程、JavaScript引擎線程、定時器觸發線程、事件觸發線程、http請求線程、合成線程和IO線程
拿到數據之后,GUI渲染線程就開始解析HTML并將其轉換成DOM(文檔對象模型),DOM是瀏覽器對頁面的內部表示,javascript獲取和操作的頁面元素本質是瀏覽器提供的DOM數據,同時當頁面發生重繪和回流的時候,該線程也會執行
在解析過程中,即便是你的html語法有一些異常,比如沒有關閉標簽,匹配錯誤等,瀏覽器也不會拋出異常,比如如下代碼,在瀏覽器上會自動解析成功
<body>
<div>
</p>
</body>
這里還需要注意一點的是,GUI渲染線程和JavaScript引擎線程是互斥的,當JavaScript引擎線程執行的時候,GUI線程是被掛起的,相當于是凍結狀態,GUI的更新會被保存在一個隊列中等JavaScript引擎空閑的時候立刻執行。之所以這樣是因為JS代碼可能會改變DOM結構,所以JavaScript引擎執行時間過長是會阻塞頁面的渲染的,了解這一點也就知道為什么fiber架構為什么能夠讓大型應用看起來不卡頓
在解析html的過程中,其實還有一些其它的資源,比如img或者link,這個時候就會給瀏覽器進程的網絡線程發送信息,GUI線程會根據這些額外的資源是否會阻塞轉換過程而決定是不是需要資源加載完畢。比如碰到script標簽有可能就會阻塞,但是也有例外,script標簽添加了async或者defer屬性
負責解析JavaScript腳本,運行代碼
比如我們的點擊事件,滾動事件,異步請求,或者執行setTimeout等這些事件時,會將對應的任務添加到事件觸發線程,當這個事件被觸發的時候,則把觸發的事件回調添加到待處理隊列的隊尾。由于javascript是單線程的,所以處理這些事件都必須排隊
setInterval與setTimeout所在線程,計數通過上面的內容,可以得到不可能通過javascript線程計數,否則會阻塞,因此會有一個單獨的線程進行計數的處理,等待時間達到后,將回調函數添加到事件隊列
在XMLHttpRequest連接后是通過瀏覽器新開一個線程請求,監測到獲取了對應的內容后,將回調函數添加到事件隊列,再由javascript引擎執行
合成線程后面會講,IO線程主要用于和其它的線程通信
當前DOM已經有了,但是精美的頁面光有DOM是不夠的,只有DOM是不會出現我們的五彩斑斕的頁面,需要CSS讓頁面變得更美觀,GUI線程會解析CSS并決定每個DOM元素的樣式
如果你沒有設置對應的樣式,瀏覽器也有自己的內置的一些標簽樣式,比如h1-h6
有了樣式,渲染進程已經知道了每個節點呈現的效果,但是節點的位置信息怎么來,這個時候需要布局樹,渲染進程會遍歷DOM結構(包含樣式),布局樹只包含在頁面中顯示的元素,當一個元素被設置為display: none的時候布局樹中是沒有這個元素的。同理如果div::before { content: 'Hi!' },則布局樹中是存在這個Hi的,DOM樹javascript能夠獲取,但是布局樹獲取不到
布局樹的描述非常具有挑戰性,因為你需要對整個頁面進行精確的描繪。布局中存在浮動、定位、固定、文字換行,自動伸縮,各種元素的結合,可以想象這個任務多么繁重
我們有了布局樹的信息之后是不是就能繪制了,其實并不是,雖然你知道了每個元素的位置,但是它們繪制的順序是怎樣的其實還是不清楚,到底哪個元素先,哪個元素后,了解PS的同學,肯定知道圖層的概念,哪個元素應該在哪個元素的頂部?CSS有控制元素層級的一個屬性,叫做z-index,用過的同學應該都了解
這個階段會通過布局樹形成繪制記錄,繪制記錄本質就是繪制的一系列步驟,比如我要先干什么,再干什么(先繪制背景,再繪制元素內容,再繪制形狀等等)
渲染的過程開銷是很大的,任何一個小的變化都會引起一系列的變化,當布局樹發生變化的時候,繪制需要重新構建頁面變化,頁面有動畫的效果的時候,每一幀都需要更新動畫內容,如果無法保證幀動畫,給用戶感官上就會出現卡頓
javascript也會阻塞頁面的渲染,導致卡頓的發生,可以將 Javascript 操作優化成小塊,然后使用requestAnimationFrame()
目前我們已經有了所有的信息,文檔結構-元素樣式-元素幾何-布局樹-繪制記錄,最終將繪制記錄轉換到屏幕上的像素稱之為光柵化
之前的方式是可視區域進行光柵化,滾動的時候再次進行光柵化,如下所示
但是現在瀏覽器有著更好的處理方式,這個方式被叫做合成
合成會將一個頁面拆成很多層,每個層在不同的的合成線程中進行光柵化,然后組合成一個新的頁面。滾動過程中如果這個層已經光柵化,則使用已經光柵化的層進行合成
那這個時候問題就來了,一個層中要包含哪些元素呢?主線程需要遍歷布局樹,作為開發者,想要創建一個新的層,可以使用css 屬性will-change讓瀏覽器創建層
光柵化各個層之后,將其存儲在GPU的緩存中,合成線程也能夠決定相應的優先級,保證用戶看到的部分最先被光柵化,每當有交互發生變化,合成線程就會創建更多的合成幀然后通過 GPU 將新的部分渲染出來
事件是什么?比如按鈕的點擊,input輸入框的內容輸入,鼠標滾輪和拖拽,都是事件
交互的時候瀏覽器進程最先接收到事件,瀏覽器關心的只有當前事件發生在哪個頁簽,然后將事件位置信息和事件類型發送到當前頁簽的渲染進程,渲染進程會找到事件發生的元素和對應的事件
但是前面也說到,頁面是被光柵化的,在合成線程處理頁面的時候,合成線程會標記有事件監聽的區域,有這些信息,合成線程就會將觸發的事件發送給主線程處理
瀏覽器的多進程架構,根據不同的功能劃分了不同的進程,進程內不同的使命劃分了不同的線程,當用戶開始瀏覽網頁時候,瀏覽器進程進行處理輸入、開始導航請求數據、請求響應數據,查找新建渲染進程,提交導航,之后渲染又進行了解析HTML構建DOM、構建過程加載子資源、下載并執行JS代碼、樣式計算、布局、繪制、合成,一步一步的構建出一個可交互的WEB頁面,瀏覽器進程又接受頁面的交互事件信息,并將其交給渲染進程,渲染進程內主進程進行命中測試,查找目標元素并執行綁定的事件,完成頁面的交互。
剛踏上開發之路時,我幾乎只關注怎樣去寫代碼、怎樣提升自己的生產效率。誠然,這些事情很重要,但與此同時我們也應當思考瀏覽器會怎么去處理我們書寫的代碼。現代瀏覽器一直致力探索如何提供更好的用戶體驗。書寫對瀏覽器友好的代碼,反過來也能提供友好的用戶體驗。希望能夠課讓大家了解瀏覽器的運行機制和原理,構建出對瀏覽器更為友好的代碼。同時也能夠不斷優化我們的業務,讓用戶體驗更上一層樓。
文章來源:FE情報局
_https://cloud.tencent.com/developer/article/2186044