Chromium 90, 给前端用的 devtools 的前端写得有性能问题

- hikerpig
#Chromium

以此拗口又搞笑的标题纪念一下人生第一次给 Chromium 项目提的 issue,在学会了 tracing 工具的使用方式之后,也会有更多提 bug issue 的机会。

缘起

4月中旬后的一周里,感觉使用 Chrome Devtools 调试非常的卡,尤其是在刚打开 Devtools 的时候卡上七八秒。心情烦躁、血压飙升、严重影响了工作效率,提升了社会不稳定因素(大雾)。

定位问题

  • 打开 Mac 的 Activity Monitor,明显发现打开 Devtools 时 Renderer 进程 CPU 飚上 100。
  • 以上情形只发生在 Devtools 打开 Console tab 的时候,如果一开始并没有打开 Console,就没有卡顿。
  • 仅在 Console tab 有大量 log 的情况下出现此问题,log 较少时正常。
  • 使用 Vivaldi 浏览器没有问题,当时它基于 Chromium 89。Chrome 90 和同样基于 90 的 Edge 稳定版出现问题,因此把定位范围缩小到了 Chromium 90 的 Devtools 。

Devtools 本身就是用于前端调试和发现潜在性能问题的工具,那么我们用什么来调试它呢?

使用 chrome://tracing

Chromium 的 Tracing 工具 可以详细录制 Chromium 各个进程的运行时方法调用和占时,包括 C++ 和 Javascript。

chrome://tracing 除了录制以外,也是一个比较好用的 trace-viewer,可用来查看和分析由其他工具(例如 Android systrace, Electron 等)录制的 trace 数据。特别说一句,它也可以用来查看 Chrome Devtools Performance 录制的数据,但感觉 Viewer 的侧重不在 Web 开发的角度,少了一些对 Web Performance 特别定制的辅助可视化,没有 Devtools Performance 本身的查看器好用。

一些参考教程如下:

开始录制

点开 chrome://tracing 页面左上角的 Record 按钮,然后

  1. 选择 Manually select settings
  2. Record Categories 的几十个选项里,勾上 devtools
  3. 点击 Record 开始录制
  4. 操作复现出 Console tab 的卡顿

下面是我随便找来的一张截图,展示一下 Record Categories 里的茫茫选项。里面的 v8/cc/blink 等,是前端工程师日常可以看一看的,可以从不同粒度了解一下 Chromium 一些运行时候的执行过程。当然这是建立在你对 Chromium 感兴趣和略有了解的基础上的,前端开发还是主要面向 Web 标准,日常还是应该使用没那么艰深的 Devtools,而不是一头扎入浏览器实现的细节中。

定位到问题

Devtools 的渲染主线程有触目惊心的近 6 秒卡顿,此时 V8 在执行 JS,看起来就🧐... 是前端的锅。

提 issue

Issues - chromium 提一个 issue,贴上截图、可重现问题的 html 文件、tracing 录制压缩包。

Chromium 缺陷追踪工具界面好难用,里里外外散发着 G 家工程向产品一贯的简陋风格。

后续

发现 chromium 91 已修复此问题

Devtools 的前端项目有一个自己的 repo,devtools/devtools-frontend - Git at Google,翻了一下发现这个 Gerrit Change 修复了此性能问题。

diff --git a/front_end/panels/console/ConsoleSidebar.ts b/front_end/panels/console/ConsoleSidebar.ts
index cd8ebaabc..3828d9ce5 100644
--- a/front_end/panels/console/ConsoleSidebar.ts
+++ b/front_end/panels/console/ConsoleSidebar.ts
@@ -164,13 +164,29 @@ export class URLGroupTreeElement extends ConsoleSidebarTreeElement {
   }
 }
 
+/**
+ * Maps the GroupName for a filter to the UIString used to render messages.
+ * Stored here so we only construct it once at runtime, rather than everytime we
+ * construct a filter or get a new message.
+ */
+const stringForFilterSidebarItemMap = new Map<GroupName, string>([
+  [GroupName.ConsoleAPI, UIStrings.dUserMessages],
+  [GroupName.All, UIStrings.dMessages],
+  [GroupName.Error, UIStrings.dErrors],
+  [GroupName.Warning, UIStrings.dWarnings],
+  [GroupName.Info, UIStrings.dInfo],
+  [GroupName.Verbose, UIStrings.dVerbose],
+]);
+
 export class FilterTreeElement extends ConsoleSidebarTreeElement {
   _selectedFilterSetting: Common.Settings.Setting<string>;
   _urlTreeElements: Map<string|null, URLGroupTreeElement>;
   _messageCount: number;
+  private uiStringForFilterCount: string;
 
   constructor(filter: ConsoleFilter, icon: UI.Icon.Icon, selectedFilterSetting: Common.Settings.Setting<string>) {
     super(filter.name, filter);
+    this.uiStringForFilterCount = stringForFilterSidebarItemMap.get(filter.name as GroupName) || '';
     this._selectedFilterSetting = selectedFilterSetting;
     this._urlTreeElements = new Map();
     this.setLeadingIcons([icon]);
@@ -178,6 +194,7 @@ export class FilterTreeElement extends ConsoleSidebarTreeElement {
     this._updateCounter();
   }
 
+
   clear(): void {
     this._urlTreeElements.clear();
     this.removeChildren();
@@ -195,21 +212,17 @@ export class FilterTreeElement extends ConsoleSidebarTreeElement {
   }
 
   _updateCounter(): void {
-    this.title = this._updateGroupTitle(this._filter.name, this._messageCount);
+    this.title = this._updateGroupTitle(this._messageCount);
     this.setExpandable(Boolean(this.childCount()));
   }
 
-  _updateGroupTitle(filterName: string, messageCount: number): string {
-    const groupTitleMap = new Map([
-      [GroupName.ConsoleAPI, i18nString(UIStrings.dUserMessages, {n: messageCount})],
-      [GroupName.All, i18nString(UIStrings.dMessages, {n: messageCount})],
-      [GroupName.Error, i18nString(UIStrings.dErrors, {n: messageCount})],
-      [GroupName.Warning, i18nString(UIStrings.dWarnings, {n: messageCount})],
-      [GroupName.Info, i18nString(UIStrings.dInfo, {n: messageCount})],
-      [GroupName.Verbose, i18nString(UIStrings.dVerbose, {n: messageCount})],
-    ]);
-    return groupTitleMap.get(filterName as GroupName) || '';
+  _updateGroupTitle(messageCount: number): string {
+    if (this.uiStringForFilterCount) {
+      return i18nString(this.uiStringForFilterCount, {n: messageCount});
+    }
+    return '';
   }
+
   onMessageAdded(viewMessage: ConsoleViewMessage): void {
     const message = viewMessage.consoleMessage();
     const shouldIncrementCounter = message.type !== SDK.ConsoleModel.MessageType.Command &&

更多阅读

对 Devtools 项目有兴趣的,可以看一下他们的 engineering 博客和贡献指南。