起因:一个"注释掉 display: block,页面就多一截空白"的怪问题
给站点加文章时,某篇带数学公式的文章页面出了怪事:
- 如果把
.math.display的display: block注释掉(公式以inline渲染),页面底部会莫名其妙地多出一截空白,占据整个屏幕宽度,在开发者工具里还选不中,位置在.container下方。 - 如果取消注释(
display: block),底部空白没了,但某一条公式(带编号标签的那条)下方会多出一个空行,而且只占一行高度却出现垂直方向的滚动条。
两条症状像跷跷板一样此消彼长,光看 CSS 完全想不通。于是开始排查。
最小复现
先把整站结构抽出来做成最小页面:同样的 .container 网格 + .content + .math.display 规则,只放一条触发公式——就是下面这条带编号标签的:
$$p(x_{t-1}\mid x_t) = \int p(x_{t-1}\mid x_t, x_0)p(x_0\mid x_t) dx_0\tag{\ast}$$
用 headless Chrome 渲染,再注入脚本测量各元素的高度:
// 复现页里测量的关键指标
mjx-container scrollHeight=369 clientHeight=44 // ← 内容比盒子高 8 倍
span display=inline overflowX=auto // inline 元素,overflow 不生效
p scrollHeight=389 clientHeight=83
全站 12 条公式里,只有带 \tag 的那条出现 scrollHeight=369 vs clientHeight=44 的巨大差距,其余公式都正常(scrollHeight ≈ clientHeight)。放到真实页面上也复现了:
scrollHeight=2098 container.bottom=1953 // 页面比容器多出约 145px
公式#10(带 \tag): scrollHeight=369 clientHeight=44
关键线索:MathJax 塞进去的"读屏用 MathML"
MathJax 渲染带 \tag 的公式时,除了可见的 CHTML 结构,还会额外生成一份给屏幕阅读器读的隐藏 MathML(<mjx-assistive-mml>)。把这部分 DOM dump 出来,结构长这样:
<mjx-assistive-mml unselectable="on" display="block">
<math display="block">
<mtable displaystyle="true">
<mlabeledtr>
<mtd><mtext>(\ast)</mtext></mtd>
<mtd>
<mi>p</mi><mo>(</mo><msub>…</msub><mo>|</mo>…
<mo>∫</mo>… <!-- 一整串裸 token,没有 <mrow> 包裹 -->
</mtd>
</mlabeledtr>
</mtable>
</math>
</mjx-assistive-mml>
测量这棵树的盒子高度:
MJX-ASSISTIVE-MML height=369 ← 幽灵本体
math height=368
mtable height=368 ← Chrome 渲染 <mtable> 给出了 368px 高
而可见公式只有 44px 高。这个 369px 的幽灵被 MathJax position:absolute 定位在 position:relative 的容器里,向上撑大了整页的滚动区。
逐 token 拆解:Chrome 把 <mtd> 里的裸元素竖着排了
为什么 <mtable> 会是 368px?把同样结构单独拎出来测(去掉 clip、让内容可见),发现了一个惊人的现象:Chrome 把 <mtd> 里每一个顶层元素都当成单独的一行,垂直往下排:
box0(MathJax 的裸 token 结构): mtable height=459
mi y=12 mo y=28 msub y=50 mo y=65
msub y=83 mo y=99 mo y=121 mo y=127 (∫, h=52) …
box1(同样的内容包一层 <mrow>): mtable height=63
mrow y=6 h=52 ← 一行搞定
控制变量:<mtd> 里放 N 个裸 token,高度随 token 数暴涨——1 个=80px,2 个=82px,4 个=98px,8 个=130px,16 个=194px;但只要包一个 <mrow>,16 个 token 也是 80px。
也就是说:Chrome 的 MathML 引擎在 <mtd> 遇到多个顶层子元素时,没有把它们按 <mrow> 处理成一行,而是错误地垂直堆叠,堆出几百像素高。这是 Chromium 的已知 bug(<mtd> 子元素的 blockify 问题,近期还在修,见文末参考),而 MathJax 对普通 \tag 公式序列化的辅助 MathML 恰好是裸 token、不包 <mrow>,于是踩中。
为什么 display: block 注释与否,症状正好相反
这 369px 幽灵是绝对的:它总在。display: block 改变的只是它被谁挡住。
注释掉(span 是 inline)→ 底部多出一截空白
overflow 对内联元素不生效,.math.display 上的 overflow-x: auto 形同虚设,没有东西能裁掉幽灵。它向下溢出,把 document 的滚动区撑大。.container 是 min-height:100vh,页面本应在容器底部结束,多出来的 145px 就成了容器下方一截空白。它不是 DOM 元素,是滚动区本身,所以全屏宽、选不中(幽灵还带 clip:rect(1px…) 和 user-select:none)。
取消注释(span 变 block)→ 空行 + 垂直滚动条
span 变成块级后 overflow-x: auto 生效;按 CSS 规范,overflow-x 非 visible 时 overflow-y 会计算成 auto,span 成了滚动容器,把幽灵裁进去。可见高度还是一行(44px),可滚动内容却有 369px → 垂直滚动条出现,滚下去还全是空白。同时 overflow-x:auto 让 span 形成 BFC,MathJax 自带的 margin: 1em 0(上下各 19.6px)不再塌陷,于是公式上下各多出约 19px 空白——这就是那个"多一个空行"。
修复
方案 A(最终采用,站点 CSS 已按此修改):把幽灵高度折叠为 0,不动可见公式,也不动读屏内容:
.content .math.display {
display: block; /* 恢复块级,让超宽公式能横向滚动 */
overflow-x: auto;
}
.content .math.display mjx-assistive-mml {
max-height: 0; /* 折叠 Chrome 算出的幽灵高度 */
}
验证结果:所有公式 scrollHeight == clientHeight,页面底部空白消失(extraBelow ≈ 0),无垂直滚动条。
方案 B:干脆不让 MathJax 生成辅助 MathML(失去读屏支持):
<script>window.MathJax = { options: { enableAssistiveMml: false } };</script>
方案 C:保持公式 inline(紧凑单行),只加 max-height: 0 一条。副作用是超宽公式无法横向滚动。
三种方案都在真实页面上验证过。最终选了 A,因为它同时保留了块级公式的横向滚动能力。
一点感想
这个 bug 单独看很难定位:它藏在"看不见的读屏内容"里,又依赖 Chrome 的一个排版错误才爆发。如果没有 headless 浏览器 + 逐个元素测量,光看 CSS 和 DOM 很难想到是 <mjx-assistive-mml> 的问题。排查工具(最小复现页 + CDP 测量脚本)留在 tmp-math-test/,以后遇到类似"高度凭空多出来"的问题可以直接复用。