DevTools 實(shí)現(xiàn)原理與性能分析實(shí)戰(zhàn)
作者:vivo 互聯(lián)網(wǎng)瀏覽器內(nèi)核團(tuán)隊(duì)-Li Qingmei
一、引言
從 2008 年 Google 釋放出第一版的 Chrome 后,整個(gè) Web 開發(fā)領(lǐng)域仿佛被注入了一股新鮮血液,漸漸打破了 IE 一家獨(dú)大的時(shí)代。Chrome 和 Firefox 是 W3C Web 標(biāo)準(zhǔn)的堅(jiān)定支持者,隨著這兩款開源瀏覽器市場(chǎng)份額逐漸加大,迎來了開發(fā)者的春天。這就迎來了一個(gè)新的職業(yè)分工——前端工程師 frontend-engineer,前端工程師促進(jìn)了 Web 應(yīng)用的繁榮,功能強(qiáng)大的調(diào)試工具必不可少。Google 基于開源的基礎(chǔ)上順勢(shì)推出了 DevTools,廣受網(wǎng)頁開發(fā)者的好評(píng),隨即也推動(dòng)了 Chrome 的在商業(yè)的成功。
本文通過分析 Chrome 的 DevTools 的技術(shù)實(shí)現(xiàn),特別是在瀏覽器內(nèi)核中的實(shí)現(xiàn)部分,來展示這款被萬千開發(fā)者所喜愛的開發(fā)工具背后的秘密。本文適合閱讀對(duì)象主要有前端開發(fā)者、有志于開發(fā) Hybrid 應(yīng)用調(diào)試工具或重寫 webdriver 實(shí)現(xiàn)對(duì) Chrome 或 WebView 控制的應(yīng)用工程師。
注:本文所有代碼分析,基于 Android Chromium 87.0.4280.141 版本分析而成。由于筆者所在團(tuán)隊(duì)主要從事 Android 平臺(tái)的 Blink 內(nèi)核開發(fā),所以分析過程主要集中在移動(dòng)端,其他平臺(tái)只是數(shù)據(jù)通路的區(qū)別,實(shí)現(xiàn)原理差別不大。
二、網(wǎng)頁調(diào)試工具發(fā)展史
2006 年之前,這屬于 IE 時(shí)代,在 IE 時(shí)代編寫 JavaScript 代碼時(shí)的調(diào)試手段,主要靠 window.alert() 或?qū)⒄{(diào)試信息輸出到網(wǎng)頁上來分析邏輯 bug。這種硬 debug 的手段,不亞于系統(tǒng)底層開發(fā),往往一個(gè)小問題要花費(fèi)掉一整天時(shí)間,開發(fā)效率極低。
2006 年 1 月份,Apple 的 WebKit 團(tuán)隊(duì)釋放出第一版本的 Web Inspector,此版本功能還比較簡(jiǎn)樸,僅可以查看 DOM 節(jié)點(diǎn)的繼承關(guān)系,節(jié)點(diǎn)所應(yīng)用了哪些 CSS 的規(guī)則。但此版本已經(jīng)奠定了今后多年的網(wǎng)頁調(diào)試工具的原型,具有劃時(shí)代意義。

WebKit 團(tuán)隊(duì)的迭代速度非常快,2006 年 6 月發(fā)布了一個(gè)重量級(jí)功能,JavaScript 的斷點(diǎn)調(diào)試功能,此時(shí)已經(jīng)具備開發(fā)者神器的雛形。
同時(shí)開源陣營出現(xiàn)一款 Firefox 的插件 Firebug,專注于 Web 開發(fā)的調(diào)試,奠定了現(xiàn)代 DevTools 的 Web UI 的布局。早期版本就支持了 JavaScript 的調(diào)試,CSS Box 模型可視化展示,支持 HTTP Archive 的性能分析等優(yōu)秀特性,后來的 DevTools 參考了此插件的功能和產(chǎn)品定位。2016 年 Firebug 整合到 Firefox 內(nèi)置調(diào)試工具,2017 年 Firebug 停止更新,一代神器就此謝幕。

此時(shí)迎來了一個(gè)開源界的狠角色 Google 團(tuán)隊(duì),基于 WebKit 加入瀏覽器研發(fā),推出的 Chrome 以「安全、極速、更穩(wěn)定」吸引了不少 IT 極客的關(guān)注,同時(shí)開發(fā)者工具這方面, Google 吸收多款調(diào)試工具的優(yōu)秀功能,推出了今天的主角 DevTools。

早期版本現(xiàn)在看起來這個(gè)布局有點(diǎn)簡(jiǎn)陋,但這可是十幾年前的作品。支持 DOM + CSS 查看,查看資源加載分析,腳本調(diào)試以及性能調(diào)試。現(xiàn)在開發(fā)中常用 DevTools 的功能,基本也就這幾個(gè)功能。
那個(gè)年代的 DevTools,基本是在跟隨 Firebug 的功能,只是交互方式上的差異。2007 年 Steve Jobs 發(fā)布了第一代 iPhone 手機(jī),Google 相繼推出了 Android 手機(jī),互聯(lián)網(wǎng)的發(fā)展來到移動(dòng)互聯(lián)網(wǎng)時(shí)代。DevTools 此時(shí)開始超越同類工具,支持了遠(yuǎn)程真機(jī)調(diào)試。Chrome 是多進(jìn)程架構(gòu),DOM 和 JavaScript 是運(yùn)行在子進(jìn)程中的,所以 DevTools 的底層實(shí)現(xiàn),已與同類產(chǎn)品完全不同。Chrome 的架構(gòu)師將 DevTools 實(shí)現(xiàn)架構(gòu)調(diào)成在 client-server 模式,這個(gè)架構(gòu)讓遠(yuǎn)程真機(jī)調(diào)試成為可能。為了方便網(wǎng)絡(luò)數(shù)據(jù)傳輸,Chrome 設(shè)計(jì)出了一套數(shù)據(jù)封裝協(xié)議 Chrome DevTools Protocal(CDP),接下來的幾年,這個(gè)架構(gòu)的調(diào)整在開源世界大放異彩。

2009 年 Ryan Dahl 基于 Chromium 的 JavaScript 虛擬機(jī) V8 設(shè)計(jì)了 Node.js,Node.js 的面世讓 JavaScript 這款 Web 腳本語言走出了瀏覽器,打開了服務(wù)端編程、桌面編程可以使用 JavaScript 語言的新局面。依托于 DevTools 的 client-server 架構(gòu)以及 Node.js 的開發(fā)者的數(shù)量不斷增加,DevTools 也迅速出圈,Chrome 團(tuán)隊(duì)于 2016 年開始支持 Node.js 的調(diào)試。DevTools 已從一款 Web 調(diào)試工具,演變成 JavaScript 生態(tài)中重要一員,助力更多的開發(fā)者開發(fā)更多優(yōu)秀代碼。Node.js 的生態(tài)都離不開 DevTools ,比如桌面開發(fā)框架 Electron、開發(fā)者喜愛的編輯器 Visual Studio Code 、前端架構(gòu) Vue.js、Facebook 開源 Android 性能分析工具 Stetho等。
三、DevTools 架構(gòu)
DevTools 是 client-server 架構(gòu),client 就是用戶操作的 Web UI 界面,負(fù)責(zé)接收用戶操作指令,然后將操作指令發(fā)往瀏覽器內(nèi)核或 Node.js 中進(jìn)行處理,并將處理結(jié)果數(shù)據(jù)展示在 Web UI 上。server 啟動(dòng)了兩類服務(wù),一種 HTTP 服務(wù);另一種 WebSocket 服務(wù)。
HTTP 服務(wù)提供內(nèi)核信息查詢能力。比如獲取內(nèi)核版本、獲取調(diào)試頁的列表、啟動(dòng)或關(guān)閉調(diào)試。
WebSocket 服務(wù)提供與內(nèi)核進(jìn)行真實(shí)數(shù)據(jù)通信的能力,負(fù)責(zé) Web UI 傳遞過來的所有操作指令的分發(fā)和處理,并將結(jié)果送回 Web UI 進(jìn)行展示。
下圖展示出了 Android DevTools 的整體架構(gòu)圖,從左側(cè)開發(fā)者通過 Web UI 的發(fā)起的操作命令,是怎么一步一步地將操作命令,傳遞到手機(jī)中的 Browser Core(Browser Core 運(yùn)行 Chrome 瀏覽器內(nèi)核的應(yīng)用,比如 Chrome 瀏覽器、Android WebView、NodeJs 應(yīng)用等)中執(zhí)行的過程。

Android 平臺(tái)巧妙地使用 ADB forward 能力,解決了 PC 上的 WebUI 與 Android 手機(jī)中的 Chrome 內(nèi)核的連接問題。輕松了實(shí)現(xiàn)了遠(yuǎn)程調(diào)試的能力,不要小瞧這一實(shí)現(xiàn),這對(duì)前端開發(fā)者效率提升是極大的。因?yàn)榍岸碎_發(fā)者的工作環(huán)境,目前來看基本是在 PC (Windows、Mac、Linux 統(tǒng)稱為 PC)下,通過遠(yuǎn)程調(diào)試能力的實(shí)現(xiàn),讓移動(dòng)端的開發(fā)實(shí)現(xiàn)了所見即所得。
正是 Chrome 團(tuán)隊(duì)基于網(wǎng)絡(luò)通信方式,作為 DevTools 底層通信框架,才為后來的 Web 開發(fā)團(tuán)隊(duì)百花齊放奠定了基礎(chǔ)。TCP/IP 是互聯(lián)網(wǎng)的基礎(chǔ),沒有哪種語言或平臺(tái)不支持 TCP/IP 的。DevTools 選型 TCP/IP 方式直接抹平了不同平臺(tái)或系統(tǒng)框架之間的差異。
Chrome DevTools Protocol(簡(jiǎn)稱CDP) 這組開放協(xié)議的推出,再一次將 DevTools 的實(shí)現(xiàn),真正做到了跨平臺(tái)。CDP 本質(zhì)就是一組 JSON 格式的數(shù)據(jù)封裝協(xié)議,JSON 是輕量的文本交換協(xié)議,可以被任何平臺(tái)任何語言進(jìn)行解析。正因?yàn)榇耍俜酵扑]的支持 CDP 的語言庫多達(dá)近十種。Google 官方推薦了 Node.js 版本 Puppeteer ,通過 Puppeteer 完整地實(shí)現(xiàn)了 CDP 協(xié)議,為 Chrome 內(nèi)核通信的方式打了一個(gè)樣,接著開源世界陸續(xù)推出了多個(gè)語言版本的 CDP 的使用庫。關(guān)于 CDP 協(xié)議,在稍后的章節(jié)會(huì)詳細(xì)介紹。

Chrome 的架構(gòu)師通過高度抽象能力,將 DevTools 的底層架構(gòu)抽象成 TCP/IP 和 CDP 兩個(gè)部分,奠定了 DevTools 的跨平臺(tái)跨終端的能力。當(dāng)年 WebSocket 的實(shí)現(xiàn)方案還處在草案階段,Chrome 架構(gòu)師就大膽地采用 WebSocket 實(shí)現(xiàn)了調(diào)試協(xié)議中的主協(xié)議部分。現(xiàn)在看來,開發(fā)者日常使用的頁面的實(shí)時(shí)截圖能力,可以實(shí)時(shí)觀察到遠(yuǎn)程網(wǎng)頁中所展示的界面,這個(gè)實(shí)時(shí)性就是基于 WebSocket 來提供的。筆者還很佩服 Chrome 架構(gòu)師的眼光和設(shè)計(jì)氣場(chǎng),正是他們優(yōu)秀的能力,將網(wǎng)頁開發(fā)者工具帶到新高度。

四、DevTools 通信協(xié)議
Chrome DevTools Protocol(簡(jiǎn)稱CDP)此協(xié)議包含兩部分 HTTP 和 WebSocket,DevTools 的 Web UI 將控制命令發(fā)往瀏覽器內(nèi)核,其中的控制命令、參數(shù)以及返回值,都是通過 CDP 來進(jìn)行封裝。命令的發(fā)送時(shí),由 Web UI 進(jìn)行封裝后,通過 WebSocket 發(fā)往瀏覽器內(nèi)核。接收到瀏覽器內(nèi)核反饋回結(jié)果后,再按協(xié)議進(jìn)行解包,分發(fā)給Web UI。

為了分析 Web UI 與 Android 瀏覽器內(nèi)核通信過程,需要做一下環(huán)境準(zhǔn)備。
4.1 環(huán)境準(zhǔn)備
為了能訪問到內(nèi)核中數(shù)據(jù),瀏覽器內(nèi)核需要開啟 DevTools Server ,PC Chrome 和 Android Chrome / WebView 的開啟方式略有不同。
PC Chrome 啟動(dòng)時(shí),增加一個(gè)啟動(dòng)參數(shù) -remote-debugging-port=9222 , 這樣 DevTools Server 就會(huì)偵聽本地的端口,可以向 http://localhost:9222 發(fā)起 HTTP / WebSocket 請(qǐng)求,即可獲取 DevTools 中的數(shù)據(jù)。
對(duì)于 Android Chrome 與 WebView 略有差異,由于 WebView 默認(rèn)是不開啟調(diào)試功能的,需要在客戶端手動(dòng)開啟,才能啟動(dòng) Server。
// Android 4.4 以上 WebView 才真正使用 Blink 內(nèi)核,所以需要在此版本及以上系統(tǒng)。if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {WebView.setWebContentsDebuggingEnabled(true);}
此時(shí) Android Chrome / WebView 在手機(jī)內(nèi)已啟動(dòng)了 Server,但為了在 PC 上能夠訪問到,需要使用 ADB工具的端口轉(zhuǎn)發(fā)能力。
ADB 端口轉(zhuǎn)發(fā)您可以使用 forward 命令設(shè)置任意端口轉(zhuǎn)發(fā),將特定主機(jī)端口上的請(qǐng)求轉(zhuǎn)發(fā)到設(shè)備上的其他端口。以下示例設(shè)置了主機(jī)端口 6100 到設(shè)備端口 7100 的轉(zhuǎn)發(fā):adb forward tcp:6100 tcp:7100通過 forward 可以打通 PC 與 Android 設(shè)備之間的網(wǎng)絡(luò)相互訪問
Android Chrome / WebView 使用 unix domain socket 建立的 Server 端,此 socket 的連接符為:
chrome_devtools_remote
和 webview_devtools_remote_分別為 chrome 和 WebView 的連接符。WebView 的連接由于可能不同應(yīng)用都使用了 WebView,所以采用了進(jìn)程 ID(PID)作為后綴來區(qū)分。
adb shell cat /proc/net/unix | grep "devtools_remote"0000000000000000: 00000002 00000000 00010000 0001 01 528176 @chrome_devtools_remote0000000000000000: 00000002 00000000 00010000 0001 01 276394 @webview_devtools_remote_23119
通過 ADB forward ,將 PC 與 Android 設(shè)備訪問打通,執(zhí)行如下命令:
# 在 PC 上偵聽 9222 端口,對(duì) localhost:9222 的請(qǐng)求將會(huì)轉(zhuǎn)發(fā)到 android 設(shè)備上的 webview_devtools_remote_23119 上adb forward tcp:9222 localabstract:webview_devtools_remote_23119
至此,就可以在 PC 上通過 9222 來訪問 Android 設(shè)備中的調(diào)試頁面了。
4.2 HTTP 協(xié)議分析
4.2.1 獲取內(nèi)核版本信息
curl http://localhost:9222/json/version{"Android-Package": "com.vivo.browser","Browser": "Chrome/87.0.4280.141","Protocol-Version": "1.3","User-Agent": "Mozilla/5.0 (Linux; Android 8.1.0; vivo X20Plus A Build/OPM1.171019.011; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/87.0.4280.141 Mobile Safari/537.36","V8-Version": "8.7.220.31","WebKit-Version": "537.36 (@9f05d1d9ee7483a73e9fe91ddcb8274ebcec9d7f)","webSocketDebuggerUrl": "ws://localhost:9222/devtools/browser"}
從上面返回值,可以得到如下幾個(gè)信息:
Android-Package,使用 WebView 應(yīng)用的包名。
Browser,內(nèi)核的版本號(hào)。
Protocol-Version,為 CDP 的協(xié)議版本,當(dāng)前版本為 1.3,從 1.0 開始,還有 1.1、1.2 等。
User-Agent,瀏覽器的 UA 信息。
V8-Version,所使用的 JavaScript 引擎版本號(hào)。
WebKit-Version,由于 Blink 內(nèi)核是基于 WebKit 537.36 版本開發(fā),所以會(huì)有此版本信息。
webSocketDebuggerUrl,這是 WebSocket 的調(diào)試 URL。
4.2.2 獲取可調(diào)試頁面列表
curl http://localhost:9222/json/list[: attached\empty\height\never_attached\screenX\screenY\visible\width\,: ,: ,: ,: ,: ,: ,:}, {: attached\empty\never_attached\screenX\screenY\visible\,: ,: ,: ,: ,: ,:} ]
返回了一個(gè) JSON 的數(shù)組,每一個(gè)調(diào)試頁占用一個(gè)數(shù)據(jù)元素,上面的返回值可以看出,筆者環(huán)境下 vivo 瀏覽器打開了兩個(gè)頁面,一個(gè) https://xw.qq.com/#news 和 about:blank。
description,是個(gè) JSON 對(duì)象,展示當(dāng)前頁面的狀態(tài)信息。比如頁面寬、高、在屏幕上的偏移,WebView 是否已經(jīng) attached 到 view 上了,只有 attach 上的頁面,才會(huì)被展示出來,能否被調(diào)試。
devtoolsFrontendUrl,此值為一個(gè) URL,就是日常使用 DevTools 的 WebUI 控制面板地址,這是個(gè) Web APP 當(dāng)訪問過一次后,會(huì)就緩存一份在瀏覽器下。此頁面托管在某個(gè)在國內(nèi)無法正常訪問地址,所以經(jīng)常會(huì)出現(xiàn)打不開面板,而顯示白屏的情況。Chrome 瀏覽器在打包時(shí)會(huì)內(nèi)置一份與當(dāng)前內(nèi)核匹配的 WebUI 版本,所以 Chrome 可以直接調(diào)試自己的頁面。
id,這是每個(gè)打開頁面隨機(jī)生成的 GUID 值,用于生成 WebSocket 鏈接,以區(qū)分不同頁面。
title,打開網(wǎng)頁的標(biāo)題,對(duì)應(yīng)網(wǎng)頁 head 中的 title 標(biāo)簽內(nèi)容。
type,頁面的類型,主要有以下幾類 page、iframe、worker 以及 service_worker 等。
URL,當(dāng)前打開的頁面 URL。
webSocketDebuggerUrl,此參數(shù)為 WebSocket 連接的 URL。
HTTP 協(xié)議還有其他幾個(gè)子命令,比如 protocol、new、activate 等,主要是頁面控制類的,就不一一介紹了。
4.2.3 WebSocket 協(xié)議分析
WebSocket 協(xié)議由四部分組成: Domain 、Method 、 Event 和 Type 。
1)Domain,命名空間,類似 C++/Java 中的命名空間或包名,用于分割不同的命令。用于將眾多子命令按類劃分,方便使用者調(diào)用,以及防止 Method 同名沖突。以 1.3 版本的 CDP 協(xié)議,一共劃分出 15 個(gè)Domain。
Browser: 用于管理瀏覽器對(duì)象。
Debugger: 用于調(diào)試 JavaScript 的分類,比如斷點(diǎn)、調(diào)用棧等。
DOM: 所有 DOM 節(jié)點(diǎn)操作都在此 Domain 下,DOM 節(jié)點(diǎn)的修改,遍歷等。
DOMDebugger: 管理 DOM 節(jié)點(diǎn)調(diào)試的 Domain,DevTools 中節(jié)點(diǎn)修改斷點(diǎn),就是通過這組 Domain 中提供的 Method 完成的。
Emulation: 此是一組環(huán)境模擬器集合,DevTools 中的修改設(shè)備尺寸、UserAgent 等是由這個(gè) Domain 實(shí)現(xiàn)。
Input: 事件分發(fā)方法的集合。
IO: I/O 流操作集合。
Log: Log 控制 Method 集合。
Network: 瀏覽器網(wǎng)絡(luò)通信數(shù)據(jù),可能通過此 Domain 進(jìn)行捕獲。
Page: 基于 Blink 中的 Page 操作 Method 集合,比如刷新,打開 URL。
Performance: 集成了性能分析 Method。
Profiler: 采樣分析器的 Method 集成在此 Domain 下。
Runtime: 與 JavaScript 通信的 Method 被集成此 Domain 下,比如執(zhí)行 JavaScript 代碼。
Security: 安全類操作,比如證書錯(cuò)誤。
Target: DevTools 連接的一些控制類 Method 在此 Domain 下。
2)Method,方法名稱,每個(gè) Domain 下都會(huì)有一組 Method,指明了具體操作瀏覽器內(nèi)核的功能。有三部分組成:名稱 、 參數(shù) 和 返回值 。與 C++/Java 中方法描述一致。
名稱:Debugger.setBreakpointByUrl;
參數(shù):lineNumber integer [,url string,urlRegex string,scriptHash string,columnNumber integer,condition string ];
返回值:breakpointId BreakpointId,actualLocation Location。
// Debugger.setBreakpointByUrl 到內(nèi)核,帶上如下參數(shù){"lineNumber":1,"url":"snippet:///Script%20snippet%20%231","columnNumber":0,"condition":""}// 將會(huì)收到內(nèi)核的返回值,返回?cái)帱c(diǎn)成功信息{"breakpointId":"1:1:0:snippet:///Script%20snippet%20%231","locations":[]}
3)Event,通知事件,網(wǎng)頁會(huì)有很多狀態(tài)通知,需要同步到 WebUI 或其他控制端上來。Event 就是用于通知這些事件的。比如 DOM 屬性發(fā)生了變化時(shí),將會(huì)收到 Dom.attributeModified 事件;將 JavaScript 傳遞到內(nèi)核去執(zhí)行時(shí),將會(huì)收到內(nèi)核發(fā)回來的 Debugger.scriptParsed 事件和參數(shù),參數(shù)如下:
{"scriptId":"238","url":"","startLine":0,"startColumn":0,"endLine":0,"embedderName":"","endColumn":7,"endLine":0,"executionContextAuxData":{"isDefault":true,"type":"default","frameId":"2059AA1A2C1A535CF4C480DC01E7FDEC"},"frameId":"2059AA1A2C1A535CF4C480DC01E7FDEC","isDefault":true,"type":"default","executionContextId":5,"hasSourceURL":false,"hash":"035a9e1738252e22523ed8f1c52d9dbf81abe278","isLiveEdit":false,"isModule":false,"length":7,"scriptId":"238","scriptLanguage":"JavaScript","sourceMapURL":"","startColumn":0,"startLine":0,"url":""}
4)Type,是 Method 或 Event 傳遞參數(shù)的復(fù)雜數(shù)據(jù)類型,這些類型與內(nèi)核的對(duì)象相對(duì)應(yīng)。比如 DOM.Node 類型就對(duì)應(yīng)著 Blink 中的 DOM 節(jié)點(diǎn)。主要屬性如下:
nodeId: NodeId 也是 Type,節(jié)點(diǎn) id,根據(jù)此值可以在內(nèi)核找到對(duì)應(yīng)的節(jié)點(diǎn)。
parentId: NodeId 也是 Type,父節(jié)點(diǎn) id 。
nodeType: integer,節(jié)點(diǎn)類型。
nodeName: string,節(jié)點(diǎn)名稱。
nodeValue:string, 節(jié)點(diǎn)內(nèi)容。
children: array,子節(jié)點(diǎn)數(shù)組。
attributes: array, 節(jié)點(diǎn)屬性數(shù)組 通過 Node 上這些屬性,就可以將 DOM 樹的節(jié)點(diǎn)在內(nèi)存占用描述出來。DevTools 的 Web UI 中 Element 面板,就是通過 DOM.getDocument Method 將一棵 DOM 樹展現(xiàn)出來。
通過 CDP 的這種數(shù)據(jù)組織方式,既可以傳遞控制命令來操作內(nèi)核,也可以接收內(nèi)核狀態(tài)通知(Event)。通過 CDP 可以讓瀏覽器做任何事情,而且得到的信息遠(yuǎn)比使用 Chrome 圖形界面還要多。因此, Google 推出 Chrome Headless 版本,被廣泛應(yīng)用于 web 自動(dòng)化測(cè)試、網(wǎng)頁爬蟲以及網(wǎng)頁沙箱等領(lǐng)域。
當(dāng)調(diào)試移動(dòng)端瀏覽器時(shí),可以實(shí)時(shí)看到移動(dòng)設(shè)備上的所瀏覽的屏幕,這是怎么做到的呢?
其實(shí),就是一張一張截圖通過 Page.screencastFrame 事件將 base64 后的圖片發(fā)回到 Web UI 中展示的。

從 Page.screencastFrame 通知事件帶回了圖片和描述信息(Meta data):
{"data":"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBw...","metadata":{"deviceHeight":604,"deviceWidth":360,"offsetTop":60,"pageScaleFactor":1,"scrollOffsetX":0,"scrollOffsetY":832.6666870117188,"timestamp":1631018056.565802},"sessionId":2}
通過描述信息,即可將此圖片的信息展示在 WebUI 上。一張截圖近 1M 的大小,由于 DevTools 利用了 WebSocket 的雙向長鏈接的特性,所以展示出來無比平滑和清晰。
4.3 DevTools 內(nèi)核實(shí)現(xiàn)
以上章節(jié),介紹了從 Web 開發(fā)者的角度出發(fā),將操作命令傳遞到移動(dòng)端 Browser Core 的一個(gè)整體流程,以及 CDP 通信協(xié)議相關(guān)內(nèi)容。本節(jié)重點(diǎn)介紹在 Browser Core 中的實(shí)現(xiàn)過程,先介紹 DevTools 在瀏覽器內(nèi)核中實(shí)現(xiàn),后面筆者會(huì)挑選 JavaScript 如何從字符串傳遞到 V8 中執(zhí)行過程,展開來進(jìn)行詳細(xì)介紹,這一行為的實(shí)現(xiàn)方案。
4.3.1 內(nèi)核架構(gòu)介紹
DevTools 以啟動(dòng)一個(gè) Web Server 為起點(diǎn),然后將調(diào)用命令發(fā)到相應(yīng)處理模塊,整體架構(gòu)圖如下:

DevTools 在內(nèi)核中大體上分為四層:
Server 層,用于接收外部網(wǎng)絡(luò)發(fā)過來的操作請(qǐng)求。
Agent 層,對(duì)于 Server 層發(fā)過來的請(qǐng)求,進(jìn)行拆解,根據(jù)操作的類型不同,再分發(fā)給不同的 Agent 來處理。
Session 層,Session 是對(duì)不同的業(yè)務(wù)模塊進(jìn)行了一層抽象。過了 Session 層后,將會(huì)進(jìn)入不同的業(yè)務(wù)模塊,可以到達(dá) V8, Blink 等。
業(yè)務(wù)層,就是具體的功能模塊,比如 V8 模塊,主要負(fù)責(zé) JavaScript 的調(diào)試相關(guān)能力的支撐。
Server 層由 DevToolsManager 這個(gè)單例對(duì)象來管理,由于是單例所以一個(gè)進(jìn)程只會(huì)存在一個(gè) Manger 對(duì)象,從而防止被重復(fù)創(chuàng)建出多個(gè),導(dǎo)致狀態(tài)錯(cuò)亂。
4.3.2 Web Server 數(shù)據(jù)接收入口
Server 收到的請(qǐng)求都會(huì)分發(fā)給 DevToolsHttpHandler 類,此類負(fù)責(zé)網(wǎng)絡(luò) Client 發(fā)過來的數(shù)據(jù)請(qǐng)求響應(yīng)和將處理結(jié)果發(fā)送回網(wǎng)絡(luò) Client, 此類有兩個(gè)重要方法 OnJsonRequest 和 OnWebSocketMessage ,分別用來處理 HTTP 協(xié)議和 WebSocket 協(xié)議。
void DevToolsHttpHandler::OnJsonRequest(int connection_id,const net::HttpServerRequestInfo& info) {// 查詢內(nèi)核版本信息if (command == "version") {base::DictionaryValue version;version.SetString("Protocol-Version",DevToolsAgentHost::GetProtocolVersion());// ...SendJson(connection_id, net::HTTP_OK, &version, std::string());return;}// 獲取內(nèi)核所支持的協(xié)議if (command == "protocol") {DecompressAndSendJsonProtocol(connection_id);return;}// 獲取可調(diào)試頁if (command == "list") {DevToolsManager* manager = DevToolsManager::GetInstance();DevToolsAgentHost::List list =manager->delegate() ? manager->delegate()->RemoteDebuggingTargets(): DevToolsAgentHost::GetOrCreateAll();RespondToJsonList(connection_id, info.GetHeaderValue("host"),std::move(list));return;}// 啟動(dòng)一個(gè)新調(diào)試if (command == "new") {// ...std::string host = info.GetHeaderValue("host");std::unique_ptr<base::DictionaryValue> dictionary(SerializeDescriptor(agent_host, host));SendJson(connection_id, net::HTTP_OK, dictionary.get(), std::string());return;}// 激活或關(guān)閉一個(gè)調(diào)試if (command == "activate" || command == "close") {// ...SendJson(connection_id, net::HTTP_NOT_FOUND, nullptr,"Unknown command: " + command);}void DevToolsHttpHandler::OnWebSocketRequest(int connection_id,const net::HttpServerRequestInfo& request) {// 創(chuàng)建調(diào)試的 Agentif (base::StartsWith(request.path, browser_guid_,base::CompareCase::SENSITIVE)) {scoped_refptr<DevToolsAgentHost> browser_agent =DevToolsAgentHost::CreateForBrowser(thread_->task_runner(),base::BindRepeating(&DevToolsSocketFactory::CreateForTethering,base::Unretained(socket_factory_.get())));connection_to_client_[connection_id] =std::make_unique<DevToolsAgentHostClientImpl>(thread_->task_runner(), server_wrapper_.get(), connection_id,browser_agent);AcceptWebSocket(connection_id, request);return;}connection_to_client_[connection_id] =std::make_unique<DevToolsAgentHostClientImpl>(thread_->task_runner(), server_wrapper_.get(), connection_id, agent);// Accept websocketAcceptWebSocket(connection_id, request);}// WebSocket 數(shù)據(jù)接收接口,所有 WebUI 的請(qǐng)求都通過此接口分發(fā)void DevToolsHttpHandler::OnWebSocketMessage(int connection_id,std::string data) {auto it = connection_to_client_.find(connection_id);if (it != connection_to_client_.end()) {it->second->OnMessage(base::as_bytes(base::make_span(data)));}}
DevToolsHttpHandler::OnJsonRequest 用于響應(yīng) HTTP 請(qǐng)求,用于查詢內(nèi)核狀態(tài),比如內(nèi)核版本、當(dāng)前支持協(xié)議,將返回完整協(xié)議內(nèi)容,方便開發(fā)者適配對(duì)應(yīng)的支持。
DevToolsHttpHandler::OnWebSocketRequest 用于接收 WebSocket 的連接,根據(jù)此方法對(duì)不同的 Agent 對(duì)象進(jìn)行了創(chuàng)建。
DevToolsHttpHandler::OnWebSocketMessage 所有調(diào)試請(qǐng)求數(shù)據(jù),都經(jīng)過此接口通過 Client 分發(fā)到不同的 Agent 上去。
Server 層數(shù)據(jù)響應(yīng)時(shí)通過上面的三個(gè)接口來達(dá)到數(shù)據(jù)接收和分發(fā)的能力。
4.3.3 JavaScript 執(zhí)行過程
V8 JavaScript 引擎用于解釋執(zhí)行網(wǎng)頁中的 JavaScript 腳本,同時(shí)也可以通過 DevTools 接收外部傳遞過來的腳本,腳本在當(dāng)前網(wǎng)頁的 Context 下執(zhí)行,所以可以通過 JavaScript 來操作網(wǎng)頁行為,比如修改 DOM 節(jié)點(diǎn)屬性。CDP 中設(shè)計(jì)了執(zhí)行 JavaScript 接口 Runtime.evaluate ,引方法的參數(shù)如下:
{allowUnsafeEvalBlockedByCSP: false,awaitPromise: false,contextId: 14,expression: "alert('hi');",generatePreview: true,includeCommandLineAPI: true,objectGroup: "console",replMode: true,returnByValue: false,silent: false}
其中,最重要的一個(gè)參數(shù)就是 expression ,此為一個(gè) string 類型的參數(shù),用于存放需要執(zhí)行的腳本內(nèi)容。上例將會(huì)在網(wǎng)頁中彈出一個(gè)內(nèi)容為 hi 的 alert 確認(rèn)框。
V8 中有個(gè)專門的模塊,V8RuntimeAgentImpl 用于支持 CDP 中 Runtime 的這個(gè) Domain,當(dāng)然也有 V8DebuggerAgentImpl 是用來支持 Debug 這個(gè) Domain 的具體實(shí)現(xiàn)。V8RuntimeAgentImpl 中 evaluate 方法,就是用于負(fù)責(zé)接收 DevTools 發(fā)過來的執(zhí)行請(qǐng)求。
void V8RuntimeAgentImpl::evaluate(const String16& expression, Maybe<String16> objectGroup,Maybe<bool> includeCommandLineAPI, Maybe<bool> silent,Maybe<int> executionContextId, Maybe<bool> returnByValue,Maybe<bool> generatePreview, Maybe<bool> userGesture,Maybe<bool> maybeAwaitPromise, Maybe<bool> throwOnSideEffect,Maybe<double> timeout, Maybe<bool> disableBreaks, Maybe<bool> maybeReplMode,Maybe<bool> allowUnsafeEvalBlockedByCSP,std::unique_ptr<EvaluateCallback> callback);
V8RuntimeAgentImpl::evaluate 會(huì)啟動(dòng)一個(gè) microtasks 來執(zhí)行腳本,最終會(huì)走到 v8::internal::Execution::Call 中,Execution 模塊會(huì)負(fù)責(zé)將腳本進(jìn)行語法解析和編譯成字節(jié)碼,最終調(diào)度到虛擬機(jī)器中運(yùn)行。

執(zhí)行流程如上圖所示,Web UI 發(fā)出執(zhí)行腳本的字符串,WebSocket 的 OnWebSocketMessage 將會(huì)收到此命令,然后通過 DevToolsSession 逐層向 V8 分發(fā)。由于 Chrome 是多進(jìn)程架構(gòu),分為Browser 進(jìn)程和 Render 進(jìn)程,之間通過 IPC 進(jìn)行通信。上圖左側(cè)在 Browser 端執(zhí)行流程,右側(cè)為 Render 端執(zhí)行流程。
Render 端的
DevToolsSession::DispatchProtocolCommand 是一個(gè)重要的分發(fā)接口,所以發(fā)到 V8 或 Blink 的控制命令,都會(huì)經(jīng)過此接口。接著就會(huì)將控制命令發(fā)送到 V8RuntimeAgentImpl,根據(jù)命令功能的不同,調(diào)度到不同功能模塊進(jìn)行處理。
4.4 網(wǎng)頁性能調(diào)優(yōu)
4.4.1 性能分析面板介紹
DevTools 提供一組功能強(qiáng)大的性能分析工具,網(wǎng)絡(luò)、JavaScript 調(diào)試、渲染、內(nèi)存以及標(biāo)準(zhǔn)支持度檢測(cè)等。下面介紹 Performance 面板中一些性能分析時(shí)的一些功能。主界面被劃分為這幾塊:

1)幀率(FPS):線性展示了做 Performance 期間,網(wǎng)頁渲染的幀率。
2)CPU 使用率:CPU 占用走勢(shì)圖
3)加載過程中截屏:定時(shí)采集了網(wǎng)頁截屏性能
4)網(wǎng)絡(luò)加載時(shí)序:展示網(wǎng)絡(luò)資源加載次序及耗時(shí)情況
5)幀耗時(shí)(Frames):展示了渲染每幀耗時(shí)情況,紅色表示存在耗時(shí)較長的幀。
6)Web Vitals 指標(biāo):Google 推薦一套性能體驗(yàn)指標(biāo),下面會(huì)詳細(xì)介紹。
7)內(nèi)核中主要線程:瀏覽器內(nèi)核中存在多個(gè)線程各有分工,當(dāng)出現(xiàn)耗時(shí)較長幀時(shí),需要在這些線程中排查,具體哪個(gè)線程在耗時(shí)。主要分為這幾個(gè):
Main,這是 Blink 主線程,負(fù)責(zé)網(wǎng)頁的排版、解析、JavaScript 執(zhí)行等。
Raster,光柵化線程,用于將渲染對(duì)象轉(zhuǎn)化成 Bitmap。
GPU,硬件加速渲染線程,將 Texture 繪制到屏幕上。
Chrome_ChildIOThread,負(fù)責(zé)網(wǎng)絡(luò)資源,文件操作。
Compositor,合成線程,負(fù)責(zé)將渲染時(shí)各個(gè)層,合成在一起然后進(jìn)行光柵化。
ThreadPoolForegoundWorker,Worker 的工作線程池。
8)信息面板:用于展示選擇模塊詳細(xì)信息,幾個(gè)指標(biāo)含義:
Loading:網(wǎng)絡(luò)請(qǐng)求和 HTML 解析耗時(shí)。
Scripting:JavaSript 解析、編譯、在虛擬機(jī)中執(zhí)行,以及 GC 耗時(shí)。
Rendering:Blink 排版渲染耗時(shí)。
Painting:繪制耗時(shí),主要包含繪制、合成、圖片解碼以及上屏。
System 和 Idle:是系統(tǒng)調(diào)度和空閑耗時(shí)。
4.4.2 性能分析常規(guī)思路
性能分析基本思路從問題入手,網(wǎng)頁常見性能問題,筆者遇到的主要有這幾種情形。
需要的資源沒有及時(shí)被請(qǐng)求回來。排除服務(wù)器問題,資源請(qǐng)求發(fā)起太晚?資源太大?
網(wǎng)頁分層太多,導(dǎo)致 Rendering 和 Painting 時(shí)間過長。
內(nèi)存占用過多,頁面過于復(fù)雜、資源多且大、JavaScript 大塊資源持有生命周期太長。
動(dòng)畫多且消失后未移除。JavaScript 的輪播動(dòng)畫、CSS 的動(dòng)畫、帶有動(dòng)畫的圖片資源,比如 GIF, SVG、WebP 等。
事件偵聽不合理。事件偵聽過多且可能被高頻觸達(dá),比如節(jié)點(diǎn)變化、Move 事件等。
總的來說,不論是網(wǎng)頁性能優(yōu)化還是 Native 程序優(yōu)化,只要協(xié)調(diào)好這兩個(gè)資源占用即可:CPU + 內(nèi)存。只要挖掘出問題點(diǎn),性能問題都會(huì)迎刃而解,問題點(diǎn)的挖掘除了源碼級(jí)別的審查,DevTools 可以助一臂之力。
針對(duì)上面總結(jié)的常規(guī)場(chǎng)景,利用 DevTools 性能分析能力,先整體上審視 Profile 圖。
網(wǎng)絡(luò)請(qǐng)求次序和時(shí)長是否合理;
Main Thread 的長任務(wù)是否合理。

從 Network 板塊觀察資源請(qǐng)求發(fā)起的順序,是否存在長耗時(shí)任務(wù),阻塞著首屏展示資源加載,如果不保證需要的及時(shí)加載,就會(huì)長時(shí)間白屏。
資源問題就緒后,就需要排查哪些長耗時(shí)任務(wù)執(zhí)行。先查看 Main Thread 中的 Long task,比如,上圖的 Long task 就是 Scripting 的占了較長時(shí)間。通過 Bottom-Up / CallTree 查看具體的耗時(shí)點(diǎn),相應(yīng)地優(yōu)化掉。
在排查具體優(yōu)化點(diǎn)時(shí),有個(gè)小技巧。通常開發(fā)環(huán)境都是在 PC 上進(jìn)行模擬,當(dāng)版本出去后,才能暴露出問題。由于移動(dòng)設(shè)備的碎片化,很多用戶的設(shè)備,性能可能并不好。那如何在開發(fā)環(huán)境優(yōu)化這類低配置機(jī)器上的表現(xiàn)呢?DevTools 提供了限流的模擬,可以限制網(wǎng)絡(luò)制式為 2G/3G,CPU 降速。

在右上角有個(gè)“設(shè)置”,展開配置項(xiàng)目,可以看到 Network 和 CPU 的限流選項(xiàng),選擇后重新錄制一下 Profile。
上面提到,網(wǎng)頁層數(shù)太多,極大地影響到網(wǎng)頁渲染性能。“網(wǎng)頁層數(shù)” 是什么意思呢?目前,瀏覽器渲染引擎為了提升網(wǎng)頁繪制性能,繪制時(shí)會(huì)對(duì)網(wǎng)頁進(jìn)行分層。這樣的好處就是,僅重繪修改過的層,其他層內(nèi)容如果沒有變化,就不需要重新繪制,直接取上次繪制結(jié)果,從而提升繪制效率。不同的 WEB 引擎分層的策略不同,通常會(huì)將普通網(wǎng)頁、CSS 動(dòng)畫、Canvas、WebGL、Fix 標(biāo)簽等各分為一層。分層會(huì)帶來渲染效率的提升,但也會(huì)帶來內(nèi)存的開銷,從而會(huì)影響到性能。DevTools 能否分析網(wǎng)頁層數(shù)嗎?可以,在上面的“設(shè)置”中有一個(gè)選項(xiàng) “Enable advanced paint instrumentation(slow)” 啟用它,重新做一次性能錄制。

在 “信息面板” 多了一個(gè) “Layers” 標(biāo)簽,選擇后將會(huì)看到網(wǎng)頁分層情況。如果存在不合理的分層,可以嘗試調(diào)整方式,將分層進(jìn)行合并,從而達(dá)到提升性能。
4.4.3 Web Vitals
Web Vitals 是 Google 推出的一套 Web 性能與體驗(yàn)兼顧的衡量標(biāo)準(zhǔn)。原先的衡量策略基本是基于 “首字” 和 “首屏” 來衡量,但從用戶角度和技術(shù)優(yōu)化角度,這兩指標(biāo)都存在這樣那樣的問題。所以, Google 推出了 Web Vitals 標(biāo)準(zhǔn),并與 DevTools 進(jìn)行配合,方便開發(fā)者在開發(fā)階段,就識(shí)別出 Web 的性能問題。由于標(biāo)準(zhǔn)一直隨著時(shí)代的發(fā)展,不斷變化,開發(fā)者一直追著指標(biāo)的變化有點(diǎn)吃不消,好在 Google 明確表示,目前推出的三個(gè)指標(biāo),短時(shí)間內(nèi)不會(huì)變,筆者就不清楚這個(gè)短時(shí)間是多長時(shí)間。
第一個(gè)指標(biāo):Largest Contentful Paint (LCP),大面積鋪滿時(shí)間點(diǎn),2.5 秒以內(nèi)算優(yōu)秀。主要是指有大面積的文字、圖片被展示出來,就算達(dá)到了 LCP。

第二個(gè)指標(biāo):First Input Delay(FID),首次可響應(yīng)外部輸入事件的時(shí)間點(diǎn),100 ms 內(nèi)算優(yōu)秀。這個(gè)指標(biāo)是從用戶使用角度出發(fā),達(dá)到 FID 的時(shí)間點(diǎn),意味著用戶可以操作網(wǎng)頁了。

第三個(gè)指標(biāo):Cumulative Layout Shift(CLS),排版跳躍指標(biāo),0.1 為優(yōu)秀。在網(wǎng)頁加載過程中,如果出現(xiàn)排好版的元素,發(fā)現(xiàn)大面積的移動(dòng)的話,這個(gè)指標(biāo)就會(huì)很高。比如網(wǎng)頁中 img 標(biāo)簽不設(shè)置寬和高,當(dāng)圖片加載完畢后,按圖片實(shí)際大小來排版本。這樣的就會(huì)觸發(fā)網(wǎng)頁重新排版,從用戶角度網(wǎng)頁被整體向下推了一個(gè)圖片高度,Google 認(rèn)為這個(gè)體驗(yàn)不好。

LCP / FID / CLS 這三個(gè)指標(biāo),本質(zhì)上是從用戶視角看網(wǎng)頁的性能衡量指標(biāo),開發(fā)者可以看看自己作品這三個(gè)指標(biāo)屬于什么水平。
五、工具在生態(tài)構(gòu)建中的重要性

圖片(數(shù)據(jù)來自 statcounter.com)
Chrome 憑借著自己優(yōu)秀的產(chǎn)品特性,安全、快速以及穩(wěn)定性,贏得了大批用戶青睞。從上圖 StatCounter 統(tǒng)計(jì)數(shù)據(jù),可以看出 Chrome 已成為絕對(duì)的瀏覽器界的一哥,理所當(dāng)然地取得商業(yè)上的成功。但是 Chrome 在開源以及生態(tài)的建立,DevTools 可謂首功一件。Google 通過 DevTools 的超越競(jìng)品的特性,吸引了大批前端開發(fā)者,轉(zhuǎn)到 Chrome 下開發(fā)自己的產(chǎn)品。早期生態(tài)產(chǎn)品是 Chrome 插件,Chrome Store 中的插件數(shù)量就可以看出它的成功。
當(dāng) Node.js 的問世,DevTools 首款支持 Node.js 的調(diào)試工具,推動(dòng)了 Node.js 的普及。然后 DevTools 依托 Node.js 迅速出圈。另一方面,開源世界也開始反哺了 DevTools 項(xiàng)目,目前支持 CDP 協(xié)議的開源方案多達(dá) 10 幾種語言,常用的語言基本都支持上了。這個(gè)領(lǐng)域目前還在飛速發(fā)展中,期待這個(gè)領(lǐng)域可以有更好的發(fā)展。
DevTools Web UI 已經(jīng)從 Chromium 倉庫中獨(dú)立出來,可以單獨(dú) Clone 下來進(jìn)行二次開發(fā),Web UI 本次限于篇幅,未做實(shí)現(xiàn)原理分析。其實(shí),Web UI 也是個(gè)非常優(yōu)秀的 Web APP,很適合前端開發(fā)者深度研究一下。
我們從優(yōu)秀開源項(xiàng)目中學(xué)習(xí)到的不僅是代碼實(shí)現(xiàn)與架構(gòu),也可以學(xué)習(xí)到更高維度的東西,比如產(chǎn)品思維以及工具思維,并落地到自己項(xiàng)目中。回顧一下網(wǎng)頁調(diào)試領(lǐng)域發(fā)展過程,從一款 JavaScript 插件,是如何演變成今天的前端開發(fā)生態(tài),其中有很多點(diǎn)值得學(xué)習(xí)。
六、結(jié)束語
筆者所在團(tuán)隊(duì)長期致力于 Chromium 內(nèi)核的研究與學(xué)習(xí),基于其衍生出來的產(chǎn)品,服務(wù)我們生態(tài)用戶,為其提供優(yōu)質(zhì)的上網(wǎng)體驗(yàn)。同時(shí),我們孵化出的 Web 瀏覽服務(wù),也為生態(tài)內(nèi)應(yīng)用提供強(qiáng)大、快速、穩(wěn)定的 Web 服務(wù)能力。如果您有興趣于 Web 底層技術(shù)研究,歡迎加入我們,與一群志同道合的小伙伴共同成長,同時(shí)也能服務(wù)好億級(jí)用戶。
七、參考文獻(xiàn)
[1] Google Chrome
[2] 10 Years of Web Inspector
[3] 10 years of Speed in Chrome
[4] Chrome DevTools
[5] Chrome DevTools Protocol protocol
[6] Web Vitals
