首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >程序员日志|沉浸式翻译大Bug:Popup样式崩了?

程序员日志|沉浸式翻译大Bug:Popup样式崩了?

作者头像
零点便利店
发布2026-08-04 17:00:55
发布2026-08-04 17:00:55
140
举报

一场看起来像 CSS Bug 的 “幽灵事故”

事情从用户群里的一张截图开始:扩展弹窗的内容还老老实实挤在左边,右侧却凭空多出一大片空白,整个面板就像被什么看不见的“隐形力量”强行给撑开了。

最初我们以为这是个普通的 CSS Bug —— 是不是哪个组件改了宽度?是不是图片没约束?

可越查越不对劲:同一个版本每次打开宽度还不一样,删掉图片就好了,连一个没有任何业务代码、只有几行 HTML 的空页面,也能稳定被撑到 800px。

但接下来的排查却让人大跌眼镜,现象极其诡异且矛盾:

同一个版本,每次打开 Popup,异常宽度居然是随机的;

把图片资源全删掉,Popup 似乎又恢复了正常;

图片
图片

隔壁 AdBlock和沉浸式翻译的 Popup 也都出现了类似变宽的现象;

图片
图片

这些截图不能证明几个案例具有同一个根因,却共同指向了浏览器宿主的autosize:Popup 的窗口尺寸并不只由最终可见的业务组件决定。

于是我们写了一个没有业务代码、没有图片、没有异步 DOM 的纯 HTML 页面,只设置了316px 宽、650px 高。

代码语言:javascript
复制
html,body {  margin: 0;}
.repro {  box-sizing: border-box;  width: 316px;  height: 650px;}

页面没有业务 CSS、脚本、图片或异步更新,唯一的特点是高度超过了 Chrome Extension Popup 的 600px 上限。结果 Popup 稳定扩展到了 800px 宽。

图片
图片

这意味着,我们面对的压根就不是一个简单的样式 Bug,而是两个最终表现极其相似、底层触发机制却完全不同的“硬核陷阱”。

到这里,两个问题终于可以拆开讨论了。

宿主黑盒: Popup 的尺寸到底是谁说了算?

在搞清原因前,先补充一个关键背景:Chrome Extension Popup 并不是一个固定大小的窗口。

官方规定它的尺寸范围是 25 × 25 到 800 × 600 像素,并且会根据内容“自动适应”。

简单来说,你在网页里的每一次变动,都会触发一套“接力棒”机制:DOM/CSS/图片变动 → 渲染引擎重新计算排版 → 算出新的尺寸 → 一路向上通知 Chrome 原生窗口调整Bubble大小。

  • Chrome Action API:Popup
  • Chromium ExtensionPopup 实现

扩展页面中的每一次布局变化,都可能沿着下面的链路改变浏览器原生 Popup 窗口:

代码语言:javascript
复制
DOM / CSS / 图片状态变化        ↓Blink 重新执行 style 和 layout        ↓FrameViewAutoSizeInfo 计算新的内容尺寸        ↓ResizeDueToAutoResize(new_size)        ↓WebView::SetPreferredSize(new_size)        ↓Chrome Views 调整 Popup Bubble

相关入口包括:

  • FrameViewAutoSizeInfo::AutoSizeIfNeeded
  • ExtensionViewViews::ResizeDueToAutoResize
  • WebView::ResizeDueToAutoResize

理解这条链路后,两个异常就不再神秘了。

第一种机制: 加载阶段的“瞬时膨胀”

1. 用最小扩展主动制造一次瞬时撑宽

这个现象可以脱离业务代码稳定复现。最小实验让业务容器始终保持 316px ,在页面加载期间临时插入一个 720px 宽、 1px 高的元素,等待 500ms 后再把它缩回:

代码语言:javascript
复制
const transient = document.createElement("div");transient.style.cssText = "width: 720px; height: 1px;";document.body.prepend(transient);
await new Promise((resolve) => setTimeout(resolve, 500));
transient.style.width = "1px";

打开 Popup 后,即使临时元素已经缩回、可见业务内容仍然只有 316px ,宿主窗口依然可能保持在扩宽后的尺寸。这里使用普通 div 是为了排除图片缓存、网络和解码时序;换成带原始尺寸的图片、未约束 SVG 或晚到的异步内容,也会经过相同的 autosize 路径。

结果哪怕元素已经缩回去了,可见内容只有316px,Chrome 窗口却依然死死卡在扩宽后的尺寸!

这个对照实验说明,瞬时横向越界本身就足以触发问题。

  1. 图片的原始尺寸可以先于约束 CSS 参与布局

假设某个组件先把远程图片插入 DOM,负责约束图片的 CSS 则在组件提交后的 effect 中注入:

代码语言:javascript
复制
组件 DOM 提交    ↓<img> 以 naturalWidth 参与普通流布局    ↓Blink 计算出更大的 preferred width    ↓Chrome 扩大 Popup    ↓effect 执行,图片才获得 width / position / overflow 约束

从最终 DOM 看,图片可能已经变成 width: 100%position: absolute ,所以事后检查经常找不到越界元素。但 Chromium 已经在更早的一帧看到过更大的内容宽度。

字体加载、异步列表、未断行文本、SVG、Canvas 以及先渲染后隐藏的组件,都可能制造类似的瞬时状态。图片只是最常见、也最容易受缓存影响的一种。

  1. 为什么会这样? Chromium 在加载期间有意“允许增大、抑制缩小”

查看 Chromium 源码发现,引擎在页面加载期有一套防抖逻辑: “允许变大,暂不允许变小”。

Blink 当前的 FrameViewAutoSizeInfo 中仍然存在明确的加载期防抖逻辑。简化后相当于:

代码语言:javascript
复制
if (!LoadEventFinished() && new_size < current_size) {  change_size = false;}

也就是说,在页面加载尚未结束时:

  • 内容要求变大,Popup 可以立即增大;
  • 中间状态要求变小,Blink 可能暂时拒绝缩小;
  • 这样可以减少资源陆续加载时窗口来回抖动。

与此同时, ExtensionViewViews 在 Popup 尚未显示时,会把 renderer 上报的尺寸保存到 pending_preferred_size_ ;页面加载完成并变为可见时,再把这个尺寸应用到 WebView。

因此,一个短暂存在的超宽状态有机会成为 Popup 第一次显示时采用的尺寸。

需要注意的是,Chromium 并没有规定“第一次宽度会永久锁死”。Popup 显示后仍会接收后续的 preferred size。异常宽度最终没有缩回,通常还意味着:

  • 加载结束前没有产生或应用有效的缩小测量;
  • 扩大的 viewport 又参与了百分比、普通块或滚动宽度计算;
  • DOM、CSS 和资源状态在不同布局轮次中形成了反馈;
  • 浏览器最终仍然认为文档的 preferred width 是扩宽后的数值。

4. 为什么每次打开得到的宽度可能不同

图片原始尺寸通常是固定的,但浏览器采样到的页面状态并不固定。每次打开 Popup 时,下列事件的先后顺序都可能变化:

  • CSS 是否已经进入 CSSOM;
  • 图片来自内存缓存、磁盘缓存还是网络;
  • 图片头信息和 natural size 何时可用;
  • 异步配置、消息和状态更新何时提交 DOM;
  • 字体、SVG 和文本换行在哪一次 layout 中稳定;
  • Popup 在哪一次 layout 后接收到 preferred size。

因此,异常宽度不是简单的“图片宽度是多少,Popup 就是多少”,而是浏览器在加载过程里看见过哪些中间布局,以及哪一次测量成为了宿主窗口的有效输入。

第二种机制:高度超过 600px 后的新 autosize 反馈

第二种机制甚至不需要任何动态资源,纯 HTML 就能触发。因为在2026年6月Chromium 对尺寸计算算法做了一次调整。

其核心变化是:

AutoSizeUsesScrollWidthForOverflow  开启时,autosize 的宽度来源从页面的 intrinsic minimum width (内在最小宽度) 转向根文档布局盒的 ScrollWidth()

代码语言:javascript
复制
int width = AutoSizeUsesScrollWidthForOverflowEnabled()    ? document_layout_box->ScrollWidth().ToInt()    : layout_view->ComputeMinimumWidth().ToInt();

简单来说,就是官方换了一把“测量尺子”:以前测的是“内容最小需要多宽”,现在改成了测量“滚动盒子的总宽度(ScrollWidth)”。

这个功能当前被标记为 stable 并默认开启:

  • runtime_enabled_features.json5

当一个 316px 宽的 Popup 高度超过 600px 时,可能形成如下反馈:

代码语言:javascript
复制
内容高度超过 600px        ↓Popup 高度被限制,并进入根文档滚动布局        ↓根 html/body 作为普通块填充当前 viewport        ↓autosize 读取 document ScrollWidth        ↓当前 viewport 宽度被再次报告为 preferred width        ↓Popup 最终稳定在最大宽度 800px

这解释了为什么最小复现页面虽然明确写着 width: 316px ,Chrome 仍然把宿主 Popup 扩展到了 800 × 600 :被测量的不再只是那个 316px 的内容块,还包括已经参与布局的根文档滚动宽度。

Chromium 随后又提交了 Reinstate some of the min-content logic for auto resizing,说明这次从 minimum width 切换到 scroll width 后,确实还需要补回一部分 min-content 约束。

我们已经把纯 HTML 最小复现、feature flag A/B 结果和截图提交给 Chromium:[Extensions] Action popup expands from 316px to 800px when content height exceeds 600px。

该问题提交时被归类到 Chromium > Platform > Extensions ,初始状态为 New ,优先级为 P2 ;后续进展以 Issue Tracker 为准。

关闭 Chromium 特性后的因果验证

为了验证以上猜想,我们启动 Chrome 并强制关闭了这个新的算法开关,在不改动一字代码的情况下,那个 316 × 650 的纯 HTML 页面立刻恢复了正常!目前我们已将该 Bug 复现案提交至 Chromium 官方社区(Issue P2 级别)。

下面的实验使用 Chrome 151.0.7922.72 正式版(arm64):

图片
图片

为了确认最小复现不是普通 CSS 问题,我们使用隔离 Profile 启动 Chrome,并关闭该特性:

代码语言:javascript
复制
open -na "Google Chrome" --args \  --user-data-dir=/tmp/chrome-popup-autosize-off \  --disable-features=AutoSizeUsesScrollWidthForOverflow

同一个 316 × 650 页面恢复了正常宽度。

图片
图片

这个实验只改变 Chromium feature flag,没有改变页面 HTML 和 CSS,因此可以把第二种异常明确归因于新的 autosize 路径。

两种 800px 问题的区别

图片
图片

如何诊断“是谁撑宽了 Popup”

只看最终 Elements 面板通常不够。更有效的方法是给 Popup 加入早期探针,按时间记录:

代码语言:javascript
复制
performance.now()window.innerWidth / outerWidthhtml.clientWidth / scrollWidthbody.clientWidth / scrollWidthPopup 根节点 clientWidth / scrollWidthMutationObserver 变更目标图片 load/error 与 naturalWidthstyle/link 插入时刻window resize 事件document.readyState

推荐按下面的顺序缩小问题:

  1. 固定业务内容,只删除远程资源,观察是否还会扩宽;
  2. 把布局 CSS 静态写入 <head> ,排除动态 CSS 时序;
  3. 用纯 HTML 替换整个 Popup,分别测试高度 599px600px601px
  4. 对比 Chrome 版本,而不是只对比扩展版本;
  5. 使用隔离 Profile 和 --disable-features=AutoSizeUsesScrollWidthForOverflow 做因果验证。

扩展开发者可以做什么

即使浏览器未来修正高度越界行为,Popup 仍然属于由外部宿主驱动的自动尺寸页面。比较稳妥的防线包括:

1. 让决定宽度的 CSS 在第一次布局前生效

不要把关键的 widthmax-widthoverflow 和图片布局规则只放在组件 effect 中动态注入。它们更适合进入 Popup 静态 CSS 或首屏同步  <style>

2. 约束横向溢出,但不要跨平台写死根宽度

如果同一套 Popup 还运行在移动端扩展中,不能直接给共享的 htmlbody 写死 316px 。更合适的做法是先建立所有平台共用的横向安全约束,再通过构建目标或明确的布局标记,只为桌面窄面板设置固定宽度:

代码语言:javascript
复制
html,body,#mount {  box-sizing: border-box;  min-width: 0;  margin: 0;  overflow-x: hidden;}
/* data-popup-layout 仅作示例,也可以由不同构建入口加载不同样式。 */:root[data-popup-layout="desktop"],:root[data-popup-layout="desktop"] body,:root[data-popup-layout="desktop"] #mount {  width: 316px;  max-width: 316px;}
:root[data-popup-layout="mobile"] body,:root[data-popup-layout="mobile"] #mount {  width: 100%;  max-width: 100vw;}

重点不是所有平台都固定为 316px ,而是桌面 Popup 的根文档不能在首帧产生意外的横向 scrollWidth ;移动端仍应按宿主 viewport 响应式布局。不要只依赖宽度 media query 判断宿主类型,因为桌面 Popup 的异常 viewport 本身就可能达到 800px。

3. 避免让根文档承担超过 600px 的滚动

对于桌面 Chrome Popup,可以让业务容器承担纵向滚动,并把根文档控制在 Popup 上限内;移动端应使用自己的 viewport 高度约束,不应照搬这里的 600px

代码语言:javascript
复制
html,body {  overflow: hidden;}
.popup-root {  box-sizing: border-box;  width: 100%;  max-width: 100%;  max-height: 600px;  overflow-y: auto;  overflow-x: hidden;}

4. 给资源元素设置首帧安全约束

代码语言:javascript
复制
img,svg,canvas {  display: block;  max-width: 100%;}

对需要绝对定位的装饰图片,最好在元素进入 DOM 前就保证相应样式已经存在。

5. 把高度边界加入回归测试

至少覆盖:

  • 冷缓存和热缓存;
  • 高度 599 / 600 / 601px
  • 图片成功、延迟和失败;
  • Chrome 当前稳定版与下一 Beta;
  • 页面加载前后的异步 DOM 更新。

结语

回头看,这次最容易踩的坑是看到 Banner、图片、某个业务版本“恰好”和问题一起出现,就急着宣布“抓到凶手了”。

真正让案情反转的,其实只是两个"做减法"的动作:把最终 DOM 和加载途中的瞬时 DOM 分开看;把扩展的版本变化和浏览器的版本变化分开验。

最后我们才确认,同一个“弹窗右边一大片空白”的表象,背后站着两套不同机制:一个来自资源、CSS 和 DOM 的加载时序,一个来自 Chromium 对 autosize WebView autosize 宽度计算方式的改动。

这次事故也带来了一条产品层面的提醒:Popup 适合承载少量、高频、立即可操作的功能,不适合不断叠加入口和信息。内容越多、结构越复杂,越容易接近浏览器的尺寸上限,也会放大资源加载和布局时序带来的风险;次要功能更适合进入二级页面或 Side Panel。幸运的是,我们已经具备远程切换新版面板的能力,因此可以及时用更简洁的新版面板替换旧版,避开这次问题的触发条件。不过,这属于产品侧的快速止血,底层的 autosize 边界仍然需要通过布局约束和回归测试持续防守。

说到底,对跑在浏览器宿主里的前端页面来说,CSS 决定的从来不只是“内容长什么样”——某些时刻,它还悄悄决定了浏览器愿意给你多大的窗口。

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一场看起来像 CSS Bug 的 “幽灵事故”
  • 宿主黑盒: Popup 的尺寸到底是谁说了算?
  • 第一种机制: 加载阶段的“瞬时膨胀”
    • 1. 用最小扩展主动制造一次瞬时撑宽
  • 第二种机制:高度超过 600px 后的新 autosize 反馈
  • 关闭 Chromium 特性后的因果验证
  • 两种 800px 问题的区别
  • 如何诊断“是谁撑宽了 Popup”
  • 扩展开发者可以做什么
    • 1. 让决定宽度的 CSS 在第一次布局前生效
    • 2. 约束横向溢出,但不要跨平台写死根宽度
    • 3. 避免让根文档承担超过 600px 的滚动
    • 4. 给资源元素设置首帧安全约束
    • 5. 把高度边界加入回归测试
  • 结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档