跳到正文

滚动卡顿复盘:隐藏浮层为什么还在定位

阅读掘金小册时遇到一个滚动明显卡顿的页面。页面上没有展开菜单,滚动期间却能看到浮层定位函数持续执行。当次检查中,7 个已经隐藏的浮层仍在监听滚动。

沿实例创建过程继续追踪,可以定位到页面所用组件封装的一条生命周期缺口:隐藏元素的尺寸通知进入更新方法,在未检查显示状态的情况下创建 Popper,并开启滚动监听。Tooltip 关闭、销毁后,同一条路径还可能把实例重新创建回来。

谁在滚动时更新

当次检查的小册页面加载了这份代码产物。其中 Popper 负责测量参考元素、计算浮层坐标和边界避让;外层的 ByteTooltip / BytePopover 负责显示、关闭以及定位实例的生命周期。

滚动进入 Popper 的 scheduleUpdate 后,通过 requestAnimationFrame 调度更新,再执行测量、modifier 和样式写入。这条耗时路径说明定位工作正在执行,还需要继续核对实例为什么处于启用状态。

2026-11-02,在全新的匿名 Chrome 环境中打开页面,观察到 2 个 Tooltip 和 5 个 Popover 都处于下列状态。业务传入的 popperOptions 均为空对象。

检查位置 当次状态 含义
组件 popperStatus false 组件处于关闭状态
浮层计算样式 display none 浮层没有显示
Popper state.eventsEnabled true 定位实例仍在监听事件

display: none 会让元素退出布局,但不会自动解绑已有的滚动监听。这里要解释的失配是:组件明确处于关闭状态,内部定位实例却仍然开启监听。初次现场的源码位置与停止监听对照见现场观察摘要弹窗。

尺寸回调绕过了显示状态

共用封装已经有关闭时调用 disableEventListeners 的 watcher。缺口出现在另一个入口:挂载时注册的尺寸观察回调,不检查 popperStatus 就调用更新方法。

下面保留 watcher、尺寸观察注册和两个更新方法。updatePopper 在有实例时调度更新,没有实例时进入创建流程;createPopper 等其余方法保留在组件生命周期源码弹窗中。

// 0266bcd.js:4:178424,Popper 组件封装的相关字段
// 保留标识符与控制流,中文注释为文章添加。
t.default = {
  // ...省略 props、data 和其他生命周期
  // 0266bcd.js:4:179166
  watch: {
    // e 是新的显示状态;这个 watcher 依赖状态值发生变化。
    popperStatus: function (e) {
      (e
        ? (this.$emit('on-show'), this.popperJS && this.popperJS.enableEventListeners(), this.updatePopper())
        : (this.$emit('on-hide'), this.popperJS && this.popperJS.disableEventListeners()),
        this.$emit('input', e));
    },
    disabled: function (e) {
      e && (this.popperStatus = !1);
    },
  },
  // autoFixPosition 默认 true;组件挂载后观察浮层尺寸。
  // 0266bcd.js:4:179590
  mounted: function () {
    var e = this;
    this.$nextTick(
      function () {
        ((0, r.default)(this, e),
          this.autoFixPosition && this.popperElm && (0, d.addResizeListener)(this.popperElm, this.handleResize));
      }.bind(this),
    );
  },
  methods: {
    // ...省略 createPopper 等方法;其完整实现见补充材料
    // 0266bcd.js:4:181769
    updatePopper: function () {
      this.popperJS
        ? (this.popperJS.scheduleUpdate(), (this.popperJS.popper.style.zIndex = c.default.nextZIndex()))
        : this.createPopper();
    },
    // 这个入口没有检查 popperStatus。
    // 0266bcd.js:4:181918
    handleResize: function () {
      this.updatePopper();
    },
  },
};

初次尺寸通知创建了隐藏实例

这次运行中,原生 ResizeObserver 对 7 个隐藏元素分别发来一次 0 × 0 通知。当时它们都还没有 Popper 实例,handleResize 随即调用 updatePopper,后者进入 createPopper,在 $nextTick 中执行构造。

Popper 1.x 的 eventsEnabled 默认开启,新实例因此注册了滚动监听。组件的 popperStatus 一直为 false,没有发生值变化;关闭分支的 watcher 不会因实例创建而再执行一次,也就没有机会纠正这份新监听。

隐藏浮层的尺寸回调怎样创建开启监听的 Popper 实例

关闭后,尺寸通知又把实例创建回来

Tooltip 的过渡结束回调会执行 destroyPopper。本次开关验证中,元素隐藏后的尺寸通知到达时,实例已经被销毁;handleResize 又走到「没有实例就创建」的分支。最终回到「显示关闭、监听开启」的状态。

这解释了为什么只在关闭 watcher 中停止监听还不够。后来的尺寸通知可以创建另一份实例,而显示状态仍然保持 false。尺寸观察本身不需要持续触发;创建后留下的滚动监听,才是后续每次滚动仍会更新的原因。

只改一个入口验证触发链

为了验证这条链,在两个独立浏览器上下文中加载相同哈希的 bundle,只在第二个上下文的响应里将 handleResize 改为:

// 受控验证用的替换方法;其余线上逻辑保持原样,不修改服务器文件。
t.default = {
  // ...省略其他配置
  methods: {
    // ...省略其他方法
    handleResize: function () {
      if (this.popperStatus) this.updatePopper();
    },
  },
};
检查项 原逻辑 尺寸回调检查显示状态
初始隐藏且启用监听的浮层 7 0
24 次真实 window 滚动触发的隐藏实例 onUpdate 168 0
一个 Tooltip 打开后 显示,监听开启 显示,监听开启
该 Tooltip 关闭并等待过渡后 隐藏,但实例重新创建且监听开启 隐藏,实例已销毁

这是状态和调用次数的受控对照,没有测量整页性能收益;匿名页面、评论数量和浏览器条件也可能随时间变化。原始尺寸通知、实例状态、替换内容和运行环境保存在组件生命周期对照记录弹窗中。

单入口替换消除了这批隐藏实例及其滚动更新,说明触发缺口位于 ByteTooltip / BytePopover 共用封装的生命周期管理:尺寸通知可以在关闭状态创建定位实例。页面传入的配置与组件显示状态没有表现出错误;当前证据支持修正组件封装中的创建、订阅条件。Popper 在这里按默认配置启动监听,并不知道外层组件的 popperStatus。

初次现场中,暂停隐藏实例的监听后,脚本耗时下降,但页面仍有较高的布局成本。因此,这条链能够解释隐藏浮层的无效更新,尚不能解释原站全部卡顿,也不能把更新次数的下降直接换算成帧率或耗时收益。

自己的组件先约束生命周期

这个案例需要约束的是所有可能启动定位的入口。只监听 visible 的变化,覆盖不了尺寸观察、过渡回调和延迟创建任务。

  • 更新前检查状态。尺寸、滚动和业务触发的更新应遵守相同的显示条件。关闭状态下不启动新的定位工作,打开过渡中的预定位需求则需要明确保留。
  • 延迟执行时再次检查。排入 $nextTick、rAF 或定时器时仍然打开,不代表任务执行时也打开。创建实例前重新核对显示、销毁状态,必要时取消过期任务。
  • 清理后检查能否重新进入。关闭和卸载时释放定位订阅,同时处理尺寸观察与未完成任务;保留 DOM 时也要防止这些入口重新创建实例。
  • 验证状态转换。除了正常打开,检查初始关闭、关闭过渡结束、快速开关和卸载前已有待执行任务的情况。重点观察清理后的实例与监听,而不只是浮层是否消失。

本次 handleResize 的单入口替换验证了触发原因。组件库完整修复还应覆盖上述异步时序与过渡需求,不能将这个局部验证直接视为全部修复验收。

组件库通常把定位订阅与显示状态关联起来,并控制更新触发条件。按 2026-11-02 核对的官方资料与开发分支源码,可以参考这些处理;具体项目仍需检查实际依赖版本。

实现 可以借鉴的处理
Floating UI 根据相关祖先滚动、尺寸变化和布局位移更新;逐帧检测默认关闭,要求只在浮层打开时启动并及时清理。
Radix UI 使用 Floating UI,默认 optimized;选择 always 才开启逐帧位置检测。
Element Plus 随 visible 切换 Popper 的事件监听;定位交给 Popper 2。

定位源码解释每次更新的成本

生命周期约束决定一次更新是否应当发生。需要显示的浮层仍然要完成定位;已经错误启动的隐藏实例,也会进入相同的测量与计算链。接下来的源码用于说明这部分工作包含哪些成本。

定位通常先读取按钮位置与浮层尺寸,再计算坐标并写入位置。加减坐标往往很便宜;如果读取发生在尚未完成的布局变更之后,浏览器可能需要先完成布局,才能返回最新尺寸。

强制同步布局指脚本读取最新几何信息时,迫使浏览器提前完成尚未处理的布局工作。反复交错布局写入与读取,可能让原本可以合并的工作执行多次,通常称为 layout thrashing。Chrome 的布局性能说明解释了这种成本。

从压缩产物对照到上游源码

这份 bundle 末尾确实声明了 //# sourceMappingURL=0266bcd.js.map。2026-11-02 检查时,JS 仍可访问且哈希与现场样本一致,但声明的 map 地址返回 404。因此,目前没有拿到这份掘金 bundle 对应的 source map。

source map 是构建工具生成的产物位置与源文件位置之间的映射。有可用映射和原始源码时,DevTools 才能显示作者使用的文件与命名;格式化本身只恢复排版。Chrome 的说明介绍了这两者的关系。

这段定位逻辑可以追到 Popper 1.x。公开的 popper.js@1.16.1 发布包带有 source map,其中 sourcesContent 保存了 61 个源文件的内容,包括 src/modifiers/preventOverflow.js。这让我们能直接读到 popperStyles、boundaries、check 等真实命名,而不必给单字母变量猜名字。

下方使用这份上游函数解释机制,关键参数、样式读写和边界公式与现场代码逐项对照,并用 120 组坐标、宽度和越界开关输入核对了结果。这个函数的对照仍不足以确定现场依赖的精确版本;上游 map 的产物坐标也不属于掘金 bundle,不能直接给它挂载使用。原始 map 检查与对照范围见source map 与上游对照记录弹窗,现场格式化代码仍保留在定位源码与行内注释弹窗中。

没有对应 map 或可核对的上游时,AST 分析、反混淆和 AI 重命名可以辅助生成阅读版,但被删除的原始名字、注释和目录结构通常不能无损恢复。此时需要把推断命名与真实源码分开标明。

preventOverflow 中哪些操作接触 DOM

读源码时先分清两个对象:data.instance.popper 是真实浮层元素,data.offsets.popper 是本轮定位的矩形数据。下面函数中的局部变量 popper 指后者,修改它的 left 并不等于立即修改元素样式。

浮层 DOM、样式对象和坐标对象的关系,以及 preventOverflow 的读写阶段

preventOverflow 负责把浮层坐标约束在边界附近。上游原注释说明,临时撤去定位样式,是为了计算文档尺寸时排除浮层自身造成的影响。随后测量边界、恢复样式,再对坐标对象逐边修正。这里完整展示上游函数及其导入,英文注释保留原文,中文说明为文章添加。

// Popper.js@1.16.1/src/modifiers/preventOverflow.js:1-89
// 提取自公开发布包 source map;英文注释为上游原文,中文注释为本文添加。
import getOffsetParent from "../utils/getOffsetParent";
import getBoundaries from "../utils/getBoundaries";
import getSupportedPropertyName from "../utils/getSupportedPropertyName";

/**
 * @function
 * @memberof Modifiers
 * @argument {Object} data - The data object generated by `update` method
 * @argument {Object} options - Modifiers configuration and options
 * @returns {Object} The data object, properly modified
 */
export default function preventOverflow(data, options) {
  let boundariesElement =
    options.boundariesElement || getOffsetParent(data.instance.popper);

  // If offsetParent is the reference element, we really want to
  // go one step up and use the next offsetParent as reference to
  // avoid to make this modifier completely useless and look like broken
  if (data.instance.reference === boundariesElement) {
    boundariesElement = getOffsetParent(boundariesElement);
  }

  // NOTE: DOM access here
  // resets the popper's position so that the document size can be calculated excluding
  // the size of the popper element itself
  // 保存并临时清空定位样式。上面的英文原注释说明,此举是为了测量不含浮层自身影响的文档尺寸。
  const transformProp = getSupportedPropertyName("transform");
  const popperStyles = data.instance.popper.style; // assignment to help minification
  const { top, left, [transformProp]: transform } = popperStyles;
  popperStyles.top = "";
  popperStyles.left = "";
  popperStyles[transformProp] = "";

  // 测量边界。若之前的样式写入使布局失效,这里可能触发同步布局。
  const boundaries = getBoundaries(
    data.instance.popper,
    data.instance.reference,
    options.padding,
    boundariesElement,
    data.positionFixed,
  );

  // NOTE: DOM access here
  // restores the original style properties after the offsets have been computed
  // 测量完成,恢复三个原定位样式。
  popperStyles.top = top;
  popperStyles.left = left;
  popperStyles[transformProp] = transform;

  options.boundaries = boundaries;

  const order = options.priority;
  // 此处的 popper 是坐标数据对象;真实 DOM 是 data.instance.popper。
  let popper = data.offsets.popper;

  const check = {
    // left / top 分支:越过较小边界时,用 Math.max 把坐标向内推。
    primary(placement) {
      let value = popper[placement];
      if (popper[placement] < boundaries[placement] && !options.escapeWithReference) {
        value = Math.max(popper[placement], boundaries[placement]);
      }
      return { [placement]: value };
    },
    // right / bottom 分支:从边界减掉宽高,换算要修正的 left / top。
    secondary(placement) {
      const mainSide = placement === "right" ? "left" : "top";
      let value = popper[mainSide];
      if (popper[placement] > boundaries[placement] && !options.escapeWithReference) {
        value = Math.min(
          popper[mainSide],
          boundaries[placement] - (placement === "right" ? popper.width : popper.height),
        );
      }
      return { [mainSide]: value };
    },
  };

  // 按配置优先级逐边检查。以下只改坐标对象,未逐项写入 DOM 样式。
  order.forEach((placement) => {
    const side = ["left", "top"].indexOf(placement) !== -1 ? "primary" : "secondary";
    popper = { ...popper, ...check[side](placement) };
  });

  data.offsets.popper = popper;

  return data;
}

前半段实际接触 DOM 的顺序是写样式、读边界、恢复样式;后半段的 primary、secondary 和优先级循环主要处理 JS 对象。后续 computeStyle、applyStyle 再把计算结果转换为最终样式并写回元素。代码来源与保留原文见上游函数原文弹窗。

用一个横向例子看两个分支:左边越界时,将 left 推到左边界;右边越界时,从右边界扣掉宽度,换算出允许的 left。两步都在修正坐标数据。

primary 和 secondary 分别怎样修正 left,示例包含边界及浮层宽度

临时清空样式有它要解决的几何问题,处理性能时不能只删掉这几次赋值。应先确认测量是否必要、哪些写入使布局失效,以及能否在保持定位结果正确的前提下合并工作。

独立实验:读写顺序怎样影响布局

下面用一个独立的 DOM 示例观察读写顺序的成本。示例中的浮层始终显示,不加载掘金组件或 Popper;它用于比较相同工作量的两种组织方式,不能用来验证原站的实例创建缺陷。

浮层挂在 body 下,锚点留在独立的文档流中。每个浮层读取一次锚点矩形、一次自身高度,再赋值 left、top。两种模式使用相同的测量、计算、写入函数和 rAF 去重调度,只改变调用顺序。

measure 只读取锚点矩形和浮层高度,返回要写入的坐标;apply 把坐标赋给样式。锚点在独立的文档流中,因此浮层位置写入不会改变后续锚点的测量结果。两种循环的区别可以直接落到这四个函数上,外围界面与调度代码省略。

// scroll-positioning/examples/demo.js:21-42
function measure(item) {
  const rect = item.anchor.getBoundingClientRect();
  const height = item.popup.offsetHeight;
  return { x: rect.left, y: rect.top - height - 2 };
}

function apply(item, position) {
  item.popup.style.left = `${position.x}px`;
  item.popup.style.top = `${position.y}px`;
}

function updateInterleaved() {
  for (const item of items) {
    const position = measure(item);
    apply(item, position);
  }
}

function updateBatched() {
  const positions = items.map(measure);
  items.forEach((item, index) => apply(item, positions[index]));
}

最小示例中 measure 和 apply 在交错循环与批量循环里的调用顺序

图中标有「布局?」的节点表示可能被迫提前执行的布局工作。批量版本的第一次读取仍可能补做尚未完成的布局,但各次读取之间没有插入浮层位置写入,浏览器也有机会合并最后一批写入后的布局。

这份批量化成立的前提是,本轮位置写入不会改变已读取的几何信息。如果写宽度会引起换行,或写高度会推动后面的锚点,就要按依赖分阶段测量,不能把所有读取机械地搬到最前面。

先在「8 · 普通负载」下切换两种模式并滚动,再选「256 · 压力负载」点击「自动对比」。压力档增加实际浮层数量,用来放大布局差异;没有人为延时或空转循环。两种模式轮流运行,避免慢方案占用同一个主线程时,把并排运行的对照方案一起拖慢。

滚动时的读写交错与批量读写正在加载代码工作区...

自动对比按交错、批量、批量、交错的顺序执行四轮(ABBA),每轮先预热 8 次,再测量 48 次。两边沿相同路径触发真实滚动,并逐次等待定位完成。页面展示的是定位回调耗时,包含读取触发的同步布局,不包含末次写入后的延迟布局与绘制。

本次采样使用 Windows、Chrome 156.0.8078.74、1100 × 1000 视口、DPR 1,无头运行并开启录屏,未设置 CPU 降速。256 个浮层的结果如下,时间会受设备、浏览器和当前负载影响。

指标 读写交错 先读后写
定位回调平均耗时,各 96 个样本 45.99 ms 0.73 ms
定位回调 P95 51.30 ms 1.00 ms
独立浏览器采样:24 次滚动的 LayoutCount 增量 6144 24

P95 表示至少 95% 的样本耗时不高于该值。耗时来自页面内 ABBA 测量;布局次数通过 Chrome DevTools Protocol(CDP)读取 Performance 计数器,来自另一组固定 24 次滚动的采样。两组统计口径分别保留在实验原始记录弹窗中。位置断言确认两种模式得到相同坐标,桌面与手机尺寸下也检查了交互和横向溢出。

这组压力实验说明,在同样的几何读取和样式赋值次数下,读写顺序能够改变浏览器的布局工作量。6144 恰好等于 256 × 24,是这份实现和本次采样的结果;不能把几何 API 调用次数直接当成所有页面的布局次数,也不能用这组收益估算掘金原站的收益。

需要显示时,再减少重复工作

简单浮层如果可以和按钮放在同一定位容器中,使用绝对定位自然跟随滚动,就能省掉一部分 JS 更新。需要挂到 body、跨容器定位和边界避让时,再使用完整的定位引擎。

实现 JS 定位时,重点检查这些位置:

  • 本轮测量是否复用。箭头、翻转和避让尽量共享已经得到的矩形,减少重复读取。跨轮缓存尺寸时要有尺寸变化通知等失效条件,不能把会随滚动变化的视口坐标一直缓存下来。
  • 写入是否改变下一轮尺寸。让浮层在定位前后保持一致的宽度约束,避免位置变化引起换行和重复测量。位置可优先通过 transform 更新,减少对布局的影响;它仍不能消除测量本身的成本。Floating UI 的布局要求与位置样式配置说明了对应条件。
  • 同一批请求是否重复执行。用待执行标记合并更新请求。每次事件都直接调用 requestAnimationFrame(update) 仍可能排入多个回调;rAF 也不会把一次昂贵计算变便宜。MDN 的滚动事件说明提醒,rAF 本身并不是降低滚动回调频率的节流器。
  • 坐标更新是否带动无关界面。把位置状态限制在浮层需要的范围,减少整页响应式更新;结果没变化时避免重复业务更新。尺寸监听回调里写尺寸,还要检查是否形成重新测量的反馈循环。

Popper 2 的更新实现会保存本轮参考元素和浮层矩形,并合并尚未执行的普通更新;插件按读、计算、写等阶段组织。不过,单个实例有阶段划分,不代表多个浮层实例之间已经实现了全局批量读写,最终仍要用性能记录检查真实页面。

总结

这次排查定位到的缺口,是组件关闭状态与定位实例监听状态脱节:尺寸通知可以创建隐藏实例,关闭后的销毁也可能被后续通知抵消。只在 watcher 中停止监听,无法约束这些重新启动定位的入口。受控浏览器中,增加尺寸回调的显示判断后,24 次滚动触发的隐藏实例更新从 168 次降为 0 次。

耗时函数与触发原因需要分别追踪。Popper 的源码说明了测量和写入发生在哪里,组件封装则决定这些工作何时启动。本例支持修正封装层的生命周期管理;原站剩余布局开销仍需继续测量。

对确实需要执行的定位,独立实验展示了另一个优化方向:在几何依赖允许的前提下组织读写,让浏览器合并布局。它可以帮助理解成本,但实验中的批量读写收益不能作为原站修复收益。

掘金小册现场观察摘要

打开原文件 site-observation.json

这是当次现场观察的整理记录,不是原始 CPU Profile。保留了目标页面、资源哈希、产物定位点、隐藏浮层状态和证据局限。线上页面后续可能变化。

观察记录与源码定位
{
  "recordType": "manual-observation-summary",
  "observedDate": "2026-11-01",
  "note": "根据当次页面调试记录整理;不是原始 CPU Profile、事件日志或完整性能追踪。",
  "pageUrl": "https://juejin.cn/book/7223230325122400288/section/7224404685799555111",
  "browser": "Edge 156",
  "devicePixelRatio": 2,
  "bundle": {
    "url": "https://lf-web-assets.juejin.cn/obj/juejin-web/xitu_juejin_web/0266bcd.js",
    "bytes": 2471378,
    "sha256": "22d84ad71517db20c780b71753b612c62cda915737e9b00fc14090f807019be3",
    "locations": [
      {
        "name": "scheduleUpdate",
        "line": 4,
        "column": 775534,
        "behavior": "requestAnimationFrame 调度 update"
      },
      {
        "name": "B / update",
        "line": 4,
        "column": 763308,
        "behavior": "测量参考元素、执行 modifiers、调用 onUpdate"
      },
      {
        "name": "preventOverflow.fn",
        "line": 4,
        "column": 768709,
        "behavior": "临时清空 top/left/transform,读取边界后恢复"
      },
      {
        "name": "updatePopper",
        "line": 4,
        "column": 181782,
        "behavior": "组件层调用 scheduleUpdate"
      }
    ]
  },
  "hiddenOverlayObservation": {
    "count": 7,
    "display": "none",
    "stateEventsEnabled": true
  },
  "controlledComparisonNotes": {
    "action": "相同滚动路径,临时停止这 7 个隐藏浮层的事件监听",
    "original": {
      "scriptDurationSeconds": 3.5,
      "taskDurationSeconds": 5.58,
      "wallSeconds": 5.6
    },
    "listenersDisabled": {
      "scriptDurationSeconds": 0.94,
      "taskDurationSeconds": 5.02,
      "wallSeconds": 5.11
    },
    "note": "一次现场对照的手工摘要,不能外推为固定收益;并非对原站源码的正式修复。"
  },
  "limits": [
    "未取得完整 CPU Profile,不能把全部卡顿归给某一次函数调用。",
    "Long Animation Frame 脚本区间可能包含回调后的微任务,不能视为该回调的纯函数自身耗时。",
    "几何 API 调用次数不等于整页重新布局次数。",
    "未以相同方式单独测量正常显示中的原站浮层;配套压力实验是独立模型。"
  ]
}
读写顺序实验的原始样本与 CDP 指标

打开原文件 comparison-results.json

记录包含浏览器、视口、浮层数量、四轮逐次回调耗时、统计口径和独立 CDP Performance 指标。自动测试包含真实 scroll 事件与两次 rAF 间的渲染机会,回调计时本身不等于完整帧耗时。

原始实验记录
{
  "measuredAt": "2026-11-02T00:17:58.365Z",
  "browser": "156.0.8078.74",
  "conditions": {
    "width": 1100,
    "height": 1000,
    "dpr": 1,
    "overlayCount": 256,
    "cpuThrottling": "none",
    "warmupScrolls": 4,
    "measuredScrolls": 24,
    "headless": true,
    "videoRecording": true
  },
  "cdp": [
    {
      "mode": "interleave",
      "metrics": {
        "LayoutCount": 6144,
        "LayoutDuration": 0.542814,
        "RecalcStyleCount": 6144,
        "RecalcStyleDuration": 0.012604,
        "ScriptDuration": 0.021001,
        "TaskDuration": 0.685001
      }
    },
    {
      "mode": "batched",
      "metrics": {
        "LayoutCount": 24,
        "LayoutDuration": 0.003271000000000024,
        "RecalcStyleCount": 24,
        "RecalcStyleDuration": 0.004742,
        "ScriptDuration": 0.008659999999999998,
        "TaskDuration": 0.09074700000000002
      }
    },
    {
      "mode": "batched",
      "metrics": {
        "LayoutCount": 24,
        "LayoutDuration": 0.004007000000000094,
        "RecalcStyleCount": 24,
        "RecalcStyleDuration": 0.005034,
        "ScriptDuration": 0.010606000000000004,
        "TaskDuration": 0.10683900000000002
      }
    },
    {
      "mode": "interleave",
      "metrics": {
        "LayoutCount": 6144,
        "LayoutDuration": 0.9778180000000001,
        "RecalcStyleCount": 6144,
        "RecalcStyleDuration": 0.031728000000000006,
        "ScriptDuration": 0.044053,
        "TaskDuration": 1.1935479999999998
      }
    }
  ],
  "automatic": {
    "schemaVersion": 1,
    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/156.0.0.0 Safari/537.36",
    "viewport": {
      "width": 1100,
      "height": 1000,
      "dpr": 1
    },
    "overlayCount": 256,
    "readsPerUpdate": 512,
    "assignmentsPerUpdate": 512,
    "timingScope": "position callback; includes forced synchronous layout; excludes deferred rendering",
    "runs": [
      {
        "mode": "interleave",
        "samplesMs": [
          44,
          43.5,
          43.20000000298023,
          45.69999999925494,
          48.400000002235174,
          45.5,
          49.5,
          48,
          46.899999998509884,
          46.599999997764826,
          43.399999998509884,
          44.19999999925494,
          43.600000001490116,
          46.30000000074506,
          46.100000001490116,
          43.19999999925494,
          43,
          41.5,
          44.70000000298023,
          43.5,
          48.600000001490116,
          46.69999999925494,
          44.899999998509884,
          42.899999998509884,
          42.69999999925494,
          44.19999999925494,
          46.80000000074506,
          45,
          45.900000002235174,
          43,
          43.100000001490116,
          44.100000001490116,
          46.80000000074506,
          44.600000001490116,
          44.399999998509884,
          44.599999997764826,
          42.599999997764826,
          46.399999998509884,
          47.30000000074506,
          45.899999998509884,
          46.69999999925494,
          42.399999998509884,
          42.100000001490116,
          43.5,
          41.69999999925494,
          47,
          48.80000000074506,
          47.899999998509884
        ]
      },
      {
        "mode": "batched",
        "samplesMs": [
          0.6999999992549419,
          0.3999999985098839,
          0.5,
          0.6999999992549419,
          0.5,
          1.1000000014901161,
          0.6999999992549419,
          0.6999999992549419,
          0.5,
          0.5,
          0.5,
          0.6000000014901161,
          0.5999999977648258,
          0.8000000007450581,
          0.5999999977648258,
          0.9000000022351742,
          0.9000000022351742,
          1,
          0.8000000007450581,
          0.9000000022351742,
          1,
          0.9000000022351742,
          0.7000000029802322,
          0.8000000007450581,
          0.7000000029802322,
          0.9000000022351742,
          0.8000000007450581,
          0.6000000014901161,
          0.6999999992549419,
          0.7000000029802322,
          0.6999999992549419,
          0.8000000007450581,
          1,
          0.8999999985098839,
          0.8000000007450581,
          0.8000000007450581,
          0.8000000007450581,
          0.8000000007450581,
          0.8000000007450581,
          0.6999999992549419,
          0.6000000014901161,
          0.6999999992549419,
          0.8000000007450581,
          0.6999999992549419,
          0.6999999992549419,
          0.6999999992549419,
          0.8000000007450581,
          0.6999999992549419
        ]
      },
      {
        "mode": "batched",
        "samplesMs": [
          0.8000000007450581,
          0.6999999992549419,
          0.7999999970197678,
          0.7999999970197678,
          0.8000000007450581,
          0.6999999992549419,
          0.5,
          0.5999999977648258,
          0.7999999970197678,
          0.6999999992549419,
          0.8999999985098839,
          0.8000000007450581,
          0.8999999985098839,
          0.8000000007450581,
          0.6999999992549419,
          0.6999999992549419,
          0.6999999992549419,
          0.5,
          0.6999999992549419,
          0.5,
          0.6000000014901161,
          0.5,
          1.0999999977648258,
          0.8000000007450581,
          0.5,
          0.7000000029802322,
          0.6000000014901161,
          0.7000000029802322,
          0.9000000022351742,
          0.6999999992549419,
          0.6999999992549419,
          0.5999999977648258,
          0.8000000007450581,
          0.6000000014901161,
          0.6999999992549419,
          0.8000000007450581,
          0.6999999992549419,
          0.7999999970197678,
          0.8000000007450581,
          0.9000000022351742,
          0.8000000007450581,
          0.6999999992549419,
          0.6999999992549419,
          0.5999999977648258,
          0.5999999977648258,
          0.5,
          0.6999999992549419,
          0.5999999977648258
        ]
      },
      {
        "mode": "interleave",
        "samplesMs": [
          46.899999998509884,
          45.20000000298023,
          52.19999999925494,
          43.19999999925494,
          42.399999998509884,
          43.30000000074506,
          43.100000001490116,
          43.899999998509884,
          47,
          47.100000001490116,
          46,
          54,
          45,
          44.70000000298023,
          47.5,
          45.399999998509884,
          49.70000000298023,
          48.5,
          44.600000001490116,
          49.600000001490116,
          47.69999999925494,
          49.899999998509884,
          47.900000002235174,
          47.30000000074506,
          50.5,
          57.600000001490116,
          46.599999997764826,
          47.599999997764826,
          46.899999998509884,
          47.80000000074506,
          47.30000000074506,
          46.399999998509884,
          44.79999999701977,
          46.30000000074506,
          43.69999999925494,
          46.099999997764826,
          47.19999999925494,
          45.899999998509884,
          54.899999998509884,
          46.20000000298023,
          51.29999999701977,
          46.80000000074506,
          45.5,
          44.100000001490116,
          44.79999999701977,
          43.100000001490116,
          44.400000002235174,
          45.80000000074506
        ]
      }
    ],
    "summary": {
      "interleave": {
        "count": 96,
        "meanMs": 45.99062499997672,
        "p95Ms": 51.29999999701977
      },
      "batched": {
        "count": 96,
        "meanMs": 0.725000000015522,
        "p95Ms": 1
      }
    }
  },
  "mobile": {
    "width": 390,
    "scrollWidth": 390,
    "count": 8
  },
  "errors": []
}
浮层定位更新与布局测量源码

打开原文件 popper-layout.annotated.js

这份阅读片段从同一份产物提取,保留组件默认配置、完整的 preventOverflow、更新入口、modifier 调用、边界测量及最终样式写入。各段按调用关系排列,外层模块和其余 helper 已省略,不能作为独立脚本运行。中文解释注释为文章添加;原标识符和表达式保持不变。

格式化并加注释的源码片段
// 源文件:https://lf-web-assets.juejin.cn/obj/juejin-web/xitu_juejin_web/0266bcd.js
// SHA-256:22d84ad71517db20c780b71753b612c62cda915737e9b00fc14090f807019be3
// 从当次产物提取,以 Prettier 格式化;中文解释注释为本文添加。
// 保留原标识符与表达式,按调用关系排列。省略外层模块、其他 modifier 及辅助函数,供阅读而非独立运行。
// 逗号表达式从左到右执行,并返回最后一项;不能把其中几次操作误认为同时发生。

// 0266bcd.js:4:178938
t.default = {
  // ...省略组件的 props、watch 和其他方法
  data: function () {
    return {
      popperJS: null,
      popperStatus: !1,
      // 组件层的默认选项把边界设为 window,覆盖库默认的 scrollParent。
      // 这里展示默认数据;调用方仍可通过 popperOptions 提供选项。
      popperOptionsData: {
        placement: this.placement,
        computeStyle: { gpuAcceleration: !0 },
        modifiers: { preventOverflow: { boundariesElement: "window" } },
      },
      _timer: null,
      _disabledAppendToBody: !1,
    };
  },
};

// 0266bcd.js:4:768668,pe.modifiers.preventOverflow
var pe = {
  // ...省略其他默认选项
  modifiers: {
    // ...省略其他 modifier
    preventOverflow: {
      // 300 是在 modifier 链中的执行顺序,!0 等价于 true。
      order: 300,
      enabled: !0,
      // data 是本轮定位数据;e 是 preventOverflow 的运行时选项。
      fn: function (data, e) {
        // t 是边界元素或边界类型;_ 是获取定位父元素的 helper。
        // 边界若恰好是参考元素自身,就继续向外取定位父元素。
        var t = e.boundariesElement || _(data.instance.popper);
        data.instance.reference === t && (t = _(t));
        // n 是浏览器支持的 transform 属性名;r 是浮层的 style。
        // o、l、c 保存现有的 top、left、transform 字符串。
        var n = K("transform"),
          r = data.instance.popper.style,
          o = r.top,
          l = r.left,
          c = r[n];
        // 写 DOM:临时清空浮层的三个定位样式。
        ((r.top = ""), (r.left = ""), (r[n] = ""));
        // 读几何信息:E 计算边界,内部通过 M、I 等函数测量。
        // 如果前面的样式改动使布局失效,读取可能在这里触发同步布局。
        // 参数依次是浮层、参考元素、内边距、边界、fixed 定位标记。
        var d = E(
          data.instance.popper,
          data.instance.reference,
          e.padding,
          t,
          data.positionFixed,
        );
        // 再次写 DOM:恢复原定位样式,再把边界 d 留在选项对象上。
        ((r.top = o), (r.left = l), (r[n] = c), (e.boundaries = d));
        // h 是边界检查顺序;f 是当前浮层的坐标和尺寸。
        // 以下 primary / secondary 只计算并合并坐标对象,不逐项写 DOM。
        var h = e.priority,
          f = data.offsets.popper,
          m = {
            // 处理 left / top:越过最小边界时把坐标向内推。
            // 此处参数 t 是边名,和外层的边界元素 t 属于不同作用域。
            primary: function (t) {
              var n = f[t];
              return (
                f[t] < d[t] && !e.escapeWithReference && (n = Math.max(f[t], d[t])),
                // T({}, t, n) 在这里生成只有一个坐标属性的对象,等价于 { [t]: n }。
                T({}, t, n)
              );
            },
            // 处理 right / bottom:换算为 left / top,再扣除浮层宽高。
            // escapeWithReference 开启时,下面的边界修正条件不成立。
            secondary: function (t) {
              var n = "right" === t ? "left" : "top",
                r = f[n];
              return (
                f[t] > d[t] &&
                  !e.escapeWithReference &&
                  (r = Math.min(f[n], d[t] - ("right" === t ? f.width : f.height))),
                T({}, n, r)
              );
            },
          };
        return (
          // 按优先级逐边修正。这里的 e 是边名,遮蔽了外层的选项参数 e。
          h.forEach(function (e) {
            var t = -1 !== ["left", "top"].indexOf(e) ? "primary" : "secondary";
            // W 在这里相当于 Object.assign:合并本次修正后的坐标。
            f = W({}, f, m[t](e));
          }),
          // 写回本轮数据并返回;后续 computeStyle / applyStyle 负责最终位置样式。
          (data.offsets.popper = f),
          data
        );
      },
      priority: ["left", "right", "top", "bottom"],
      padding: 5,
      // 这是库默认值,可被组件和调用方覆盖;组件默认配置见配套源码。
      boundariesElement: "scrollParent",
    },
  },
};

// 0266bcd.js:4:763308
// B 是定位更新入口:先构造 data,测量参考元素并生成浮层初始坐标,再交给 D。
// O 计算参考元素偏移,P 选择 placement,F 生成浮层初始矩形。
function B() {
  if (!this.state.isDestroyed) {
    var data = {
      instance: this,
      styles: {},
      arrowStyles: {},
      attributes: {},
      flipped: !1,
      offsets: {},
    };
    ((data.offsets.reference = O(
      this.state,
      this.popper,
      this.reference,
      this.options.positionFixed,
    )),
      (data.placement = P(
        this.options.placement,
        data.offsets.reference,
        this.popper,
        this.reference,
        this.options.modifiers.flip.boundariesElement,
        this.options.modifiers.flip.padding,
      )),
      (data.originalPlacement = data.placement),
      (data.positionFixed = this.options.positionFixed),
      (data.offsets.popper = F(this.popper, data.offsets.reference, data.placement)),
      (data.offsets.popper.position = this.options.positionFixed ? "fixed" : "absolute"),
      (data = D(this.modifiers, data)),
      this.state.isCreated
        ? this.options.onUpdate(data)
        : ((this.state.isCreated = !0), this.options.onCreate(data)));
  }
}

// 0266bcd.js:4:762824
// D 依次执行 modifier;e 是 modifier 数组,t 是可选的截断位置。
// 数组在实例初始化时按 order 排序;这里仅执行 enabled 且 fn 可调用的项目。
function D(e, data, t) {
  return (
    (void 0 === t
      ? e
      : e.slice(
          0,
          (function (e, t, n) {
            if (Array.prototype.findIndex)
              return e.findIndex(function (e) {
                return e[t] === n;
              });
            var r = j(e, function (e) {
              return e[t] === n;
            });
            return e.indexOf(r);
          })(e, "name", t),
        )
    ).forEach(function (e) {
      e.function && console.warn("`modifier.function` is deprecated, use `modifier.fn`!");
      var t = e.function || e.fn;
      e.enabled &&
        c(t) &&
        ((data.offsets.popper = G(data.offsets.popper)),
        (data.offsets.reference = G(data.offsets.reference)),
        // 调用具体 modifier,包括正文展示的 preventOverflow.fn。
        (data = t(data, e)));
    }),
    data
  );
}

// 0266bcd.js:4:760717
// E 计算边界:e 是浮层,t 是参考元素,n 是 padding,r 是边界元素或类型。
// 第五个参数通过 arguments[4] 读取,o 表示是否为 fixed 定位。
// c 是几何计算参照;L / x 分别处理 fixed 参照和共同定位祖先。
function E(e, t, n, r) {
  var o = arguments.length > 4 && void 0 !== arguments[4] && arguments[4],
    l = { top: 0, left: 0 },
    c = o ? L(e) : x(e, t);
  if ("viewport" === r) l = A(c, o);
  else {
    var d = void 0;
    "scrollParent" === r
      ? "BODY" === (d = f(h(t))).nodeName && (d = e.ownerDocument.documentElement)
      : (d = "window" === r ? e.ownerDocument.documentElement : r);
    // 非 viewport 分支先选出边界节点 d,再测量它相对参照 c 的矩形。
    var m = M(d, c, o);
    if ("HTML" !== d.nodeName || N(c)) l = m;
    else {
      var v = V(e.ownerDocument),
        y = v.height,
        _ = v.width;
      ((l.top += m.top - m.marginTop),
        (l.bottom = y + m.top),
        (l.left += m.left - m.marginLeft),
        (l.right = _ + m.left));
    }
  }
  // 以下只根据 padding 收缩边界对象,不修改元素样式。
  var w = "number" == typeof (n = n || 0);
  return (
    (l.left += w ? n : n.left || 0),
    (l.top += w ? n : n.top || 0),
    (l.right -= w ? n : n.right || 0),
    (l.bottom -= w ? n : n.bottom || 0),
    l
  );
}

// 0266bcd.js:4:759397
// M 把元素 e 的矩形换算到参照元素 t 的坐标系,兼顾边框、margin 和滚动。
function M(e, t) {
  var n = arguments.length > 2 && void 0 !== arguments[2] && arguments[2],
    r = y(10),
    o = "HTML" === t.nodeName,
    // 读取两份矩形。I 内部会调用 getBoundingClientRect 和尺寸属性。
    l = I(e),
    c = I(t),
    h = f(e),
    // d(t) 读取计算样式,下面使用参照元素的边框与 margin。
    m = d(t),
    v = parseFloat(m.borderTopWidth, 10),
    _ = parseFloat(m.borderLeftWidth, 10);
  n && o && ((c.top = Math.max(c.top, 0)), (c.left = Math.max(c.left, 0)));
  var w = G({
    top: l.top - c.top - v,
    left: l.left - c.left - _,
    width: l.width,
    height: l.height,
  });
  if (((w.marginTop = 0), (w.marginLeft = 0), !r && o)) {
    var x = parseFloat(m.marginTop, 10),
      S = parseFloat(m.marginLeft, 10);
    ((w.top -= v - x),
      (w.bottom -= v - x),
      (w.left -= _ - S),
      (w.right -= _ - S),
      (w.marginTop = x),
      (w.marginLeft = S));
  }
  return (
    (r && !n ? t.contains(h) : t === h && "BODY" !== h.nodeName) && (w = k(w, t)),
    w
  );
}

// 0266bcd.js:4:758788
// I 读取 DOM 几何信息,并做浏览器兼容与尺寸修正;G 补出 right、bottom。
// 只有读取需要更新的布局时才可能触发同步布局,不是每次 API 调用都必然重排。
function I(element) {
  var rect = {};
  try {
    if (y(10)) {
      rect = element.getBoundingClientRect();
      var e = S(element, "top"),
        t = S(element, "left");
      ((rect.top += e), (rect.left += t), (rect.bottom += e), (rect.right += t));
    } else rect = element.getBoundingClientRect();
  } catch (e) {}
  var n = {
      left: rect.left,
      top: rect.top,
      width: rect.right - rect.left,
      height: rect.bottom - rect.top,
    },
    r = "HTML" === element.nodeName ? V(element.ownerDocument) : {},
    o = r.width || element.clientWidth || n.right - n.left,
    l = r.height || element.clientHeight || n.bottom - n.top,
    c = element.offsetWidth - o,
    h = element.offsetHeight - l;
  if (c || h) {
    var f = d(element);
    ((c -= Z(f, "x")), (h -= Z(f, "y")), (n.width -= c), (n.height -= h));
  }
  return G(n);
}

// 0266bcd.js:4:765735
// ie 是最终样式写入 helper:尺寸和位置的数值补 px,其余属性直接写 style。
function ie(element, e) {
  Object.keys(e).forEach(function (t) {
    var n = "";
    (-1 !== ["width", "height", "top", "right", "bottom", "left"].indexOf(t) &&
      ne(e[t]) &&
      (n = "px"),
      (element.style[t] = e[t] + n));
  });
}

// 0266bcd.js:4:774779
var pe = {
  modifiers: {
    // applyStyle 排在 order 900,接收前面计算的 data.styles,并通过 ie 写回 DOM。
    // 保留 onLoad 初始化分支,避免把初始化写入和滚动时 fn 的写入混为一谈。
    applyStyle: {
      order: 900,
      enabled: !0,
      fn: function (data) {
        var element, e;
        return (
          ie(data.instance.popper, data.styles),
          (element = data.instance.popper),
          (e = data.attributes),
          Object.keys(e).forEach(function (t) {
            !1 !== e[t] ? element.setAttribute(t, e[t]) : element.removeAttribute(t);
          }),
          data.arrowElement &&
            Object.keys(data.arrowStyles).length &&
            ie(data.arrowElement, data.arrowStyles),
          data
        );
      },
      onLoad: function (e, t, n, r, o) {
        var l = O(o, t, e, n.positionFixed),
          c = P(
            n.placement,
            l,
            t,
            e,
            n.modifiers.flip.boundariesElement,
            n.modifiers.flip.padding,
          );
        return (
          t.setAttribute("x-placement", c),
          ie(t, { position: n.positionFixed ? "fixed" : "absolute" }),
          n
        );
      },
      gpuAcceleration: void 0,
    },
  },
};
source map 公开地址检查与上游函数对照

打开原文件 source-map-investigation.json

记录包含掘金 JS 和 map 的地址、状态、哈希,以及已获取的 Popper 发布包 map 中的源文件数量、所选源码位置和哈希。120 组对照隔离了边界测量,核对了坐标修正、测量参数和样式恢复,不能据此确定整个依赖版本。source map 的映射归属于具体产物;其他构建的 map 不能直接复用到现场 bundle。

检查结果与验证边界
{
  "checkedAt": "2026-11-02T01:23:30.327Z",
  "siteBundle": {
    "url": "https://lf-web-assets.juejin.cn/obj/juejin-web/xitu_juejin_web/0266bcd.js",
    "status": 200,
    "sha256": "22d84ad71517db20c780b71753b612c62cda915737e9b00fc14090f807019be3",
    "sourceMappingUrls": [
      "0266bcd.js.map"
    ],
    "sourceMapHeader": null,
    "xSourceMapHeader": null
  },
  "declaredMap": {
    "url": "https://lf-web-assets.juejin.cn/obj/juejin-web/xitu_juejin_web/0266bcd.js.map",
    "status": 404,
    "contentType": "application/json",
    "error": "The specified key does not exist."
  },
  "retrievedUpstreamMap": {
    "url": "https://unpkg.com/popper.js@1.16.1/dist/umd/popper.js.map",
    "sha256": "1408c0eb5570ed3279b7d6a9a6f5b3c1cdeb6e6a67e2ec60cfb49bfd608dcd92",
    "bytes": 140747,
    "version": 3,
    "sources": 61,
    "sourcesContent": 61,
    "names": 381,
    "selectedSourceIndex": 54,
    "selectedSourcePath": "../../src/modifiers/preventOverflow.js",
    "selectedSourceSha256": "e97d67ceabc87a119f95c58f45daacfab9fdb409b0fa54d99f5a3268fa2141e9"
  },
  "comparison": {
    "sameNormalizedMinifiedCode": false,
    "behaviorCases": 120,
    "behaviorsEqual": true,
    "scope": "对比 preventOverflow 函数;用桩隔离边界测量,核对坐标、样式恢复、测量时样式与调用参数。不能证明整个依赖版本相同。",
    "aliases": {
      "_": "getOffsetParent",
      "K": "getSupportedPropertyName",
      "E": "getBoundaries",
      "T": "_defineProperty",
      "W": "_extends"
    }
  },
  "limits": [
    "没有取得掘金 bundle 对应的 source map,不能直接建立其压缩位置到作者源文件的位置映射。",
    "上游 1.16.1 仅作为已核对的函数对照,不能据此锁定现场依赖版本或恢复掘金业务源码。",
    "120 组对照隔离了边界测量,并核对坐标、样式恢复及测量调用;不能替代完整浏览器几何验证。"
  ]
}
Popper 1.16.1 的 preventOverflow 原始源码

打开原文件 preventOverflow.js

源文件从公开发布包的 source map 的 sourcesContent 提取,未添加正文中的中文注释。原路径为 src/modifiers/preventOverflow.js,包的 source map URL 和文件哈希保留在对照记录中。源码版权归 Federico Zivolo,按 MIT 许可提供;这里保存的是上游文件,并非掘金业务源码的还原结果。

上游源码原文
import getOffsetParent from '../utils/getOffsetParent';
import getBoundaries from '../utils/getBoundaries';
import getSupportedPropertyName from '../utils/getSupportedPropertyName';

/**
 * @function
 * @memberof Modifiers
 * @argument {Object} data - The data object generated by `update` method
 * @argument {Object} options - Modifiers configuration and options
 * @returns {Object} The data object, properly modified
 */
export default function preventOverflow(data, options) {
  let boundariesElement =
    options.boundariesElement || getOffsetParent(data.instance.popper);

  // If offsetParent is the reference element, we really want to
  // go one step up and use the next offsetParent as reference to
  // avoid to make this modifier completely useless and look like broken
  if (data.instance.reference === boundariesElement) {
    boundariesElement = getOffsetParent(boundariesElement);
  }

  // NOTE: DOM access here
  // resets the popper's position so that the document size can be calculated excluding
  // the size of the popper element itself
  const transformProp = getSupportedPropertyName('transform');
  const popperStyles = data.instance.popper.style; // assignment to help minification
  const { top, left, [transformProp]: transform } = popperStyles;
  popperStyles.top = '';
  popperStyles.left = '';
  popperStyles[transformProp] = '';

  const boundaries = getBoundaries(
    data.instance.popper,
    data.instance.reference,
    options.padding,
    boundariesElement,
    data.positionFixed
  );

  // NOTE: DOM access here
  // restores the original style properties after the offsets have been computed
  popperStyles.top = top;
  popperStyles.left = left;
  popperStyles[transformProp] = transform;

  options.boundaries = boundaries;

  const order = options.priority;
  let popper = data.offsets.popper;

  const check = {
    primary(placement) {
      let value = popper[placement];
      if (
        popper[placement] < boundaries[placement] &&
        !options.escapeWithReference
      ) {
        value = Math.max(popper[placement], boundaries[placement]);
      }
      return { [placement]: value };
    },
    secondary(placement) {
      const mainSide = placement === 'right' ? 'left' : 'top';
      let value = popper[mainSide];
      if (
        popper[placement] > boundaries[placement] &&
        !options.escapeWithReference
      ) {
        value = Math.min(
          popper[mainSide],
          boundaries[placement] -
            (placement === 'right' ? popper.width : popper.height)
        );
      }
      return { [mainSide]: value };
    },
  };

  order.forEach(placement => {
    const side =
      ['left', 'top'].indexOf(placement) !== -1 ? 'primary' : 'secondary';
    popper = { ...popper, ...check[side](placement) };
  });

  data.offsets.popper = popper;

  return data;
}
ByteTooltip 与 BytePopover 共用的 Popper 封装

打开原文件 component-lifecycle.annotated.js

从相同哈希的 0266bcd.js 提取默认配置、watch、挂载与销毁生命周期,以及实例创建、销毁、更新和尺寸回调。保留产物中的标识符和控制流;各段注明压缩资源行列,外围模块、导入和其他方法已省略,片段不能独立运行。createPopper 内的 l.default 为该模块导入的 Popper 构造器,d.addResizeListener 由 ResizeObserver 通知尺寸变化。

格式化后的组件生命周期源码
// 0266bcd.js:4:178424,Popper 组件封装的相关字段
// 保留标识符与控制流,中文注释为文章添加。
t.default = {
  props: {
    // ...省略其余 props
    // 0266bcd.js:4:178895
    autoFixPosition: { type: Boolean, default: !0 },
  },
  // 0266bcd.js:4:178938
  data: function () {
    return {
      popperJS: null,
      popperStatus: !1,
      popperOptionsData: {
        placement: this.placement,
        computeStyle: { gpuAcceleration: !0 },
        modifiers: { preventOverflow: { boundariesElement: "window" } },
      },
      _timer: null,
      _disabledAppendToBody: !1,
    };
  },
  // 0266bcd.js:4:179166
  watch: {
    popperStatus: function (e) {
      (e
        ? (this.$emit("on-show"),
          this.popperJS && this.popperJS.enableEventListeners(),
          this.updatePopper())
        : (this.$emit("on-hide"), this.popperJS && this.popperJS.disableEventListeners()),
        this.$emit("input", e));
    },
    disabled: function (e) {
      e && (this.popperStatus = !1);
    },
  },
  // 0266bcd.js:4:179590
  mounted: function () {
    var e = this;
    this.$nextTick(
      function () {
        ((0, r.default)(this, e),
          this.autoFixPosition &&
            this.popperElm &&
            (0, d.addResizeListener)(this.popperElm, this.handleResize));
      }.bind(this),
    );
  },
  // 0266bcd.js:4:179778
  beforeDestroy: function () {
    (this.destroyPopper(),
      this.autoFixPosition &&
        this.popperElm &&
        (0, d.removeResizeListener)(this.popperElm, this.handleResize));
  },
  methods: {
    // ...省略鼠标事件、延时开关和箭头添加等方法
    // 0266bcd.js:4:180404
    destroyPopper: function () {
      (this.popperJS && (this.popperJS.destroy(), (this.popperJS = null)),
        this.appendedToBody &&
          ((this.appendedToBody = !1), document.body.removeChild(this.popperElm)));
    },
    // 0266bcd.js:4:180578
    createPopper: function () {
      var e = this;
      this.$nextTick(
        function () {
          if (
            ((0, r.default)(this, e),
            this.visibleArrow && this.appendArrow(this.popperElm),
            !this.appendToBody ||
              this.appendedToBody ||
              this._disabledAppendToBody ||
              ((this.appendedToBody = !0), document.body.appendChild(this.popperElm)),
            this.popperJS && this.popperJS.destroy && this.popperJS.destroy(),
            this.boundariesSelector)
          ) {
            var t = document.querySelector(this.boundariesSelector);
            t &&
              ((this.popperOptionsData.modifiers = (0, o.default)(
                {},
                this.popperOptionsData.modifiers,
              )),
              (this.popperOptionsData.modifiers.preventOverflow = (0, o.default)(
                {},
                this.popperOptionsData.modifiers.preventOverflow,
              )),
              (this.popperOptionsData.modifiers.preventOverflow.boundariesElement = t));
          }
          ((this.popperOptionsData.onCreate = function () {
            ((0, r.default)(this, e), this.$nextTick(this.updatePopper));
          }.bind(this)),
            (this.popperJS = new l.default(
              this.referenceElm,
              this.popperElm,
              this.popperOptionsData,
            )),
            (this.popperJS.popper.style.width = this.width),
            (this.popperJS.popper.style["max-width"] = this.maxWidth));
        }.bind(this),
      );
    },
    // 0266bcd.js:4:181769
    updatePopper: function () {
      this.popperJS
        ? (this.popperJS.scheduleUpdate(),
          (this.popperJS.popper.style.zIndex = c.default.nextZIndex()))
        : this.createPopper();
    },
    // 0266bcd.js:4:181918
    handleResize: function () {
      this.updatePopper();
    },
  },
};
隐藏实例创建与滚动更新的受控对照

打开原文件 component-lifecycle-comparison.json

两个全新匿名 Chrome 上下文分别加载原 bundle 和仅替换 handleResize 的 bundle。记录尺寸通知到达前的实例状态、24 次真实 window 滚动的事件与 onUpdate 次数,以及一个 Tooltip 的开关结果。网络替换只发生在受控浏览器内,不修改服务器。结果用于核对触发链,不代表完整 CPU Profile 或整页性能收益。

生命周期状态、尺寸通知与对照结果
{
  "observedAt": "2026-11-02T01:55:06.425Z",
  "pageUrl": "https://juejin.cn/book/7223230325122400288/section/7224404685799555111",
  "bundleUrl": "https://lf-web-assets.juejin.cn/obj/juejin-web/xitu_juejin_web/0266bcd.js",
  "browser": "156.0.8078.74",
  "viewport": {
    "width": 1280,
    "height": 960
  },
  "needle": "handleResize:function(){this.updatePopper()}",
  "replacement": "handleResize:function(){if(this.popperStatus)this.updatePopper()}",
  "scope": "全新匿名浏览器上下文;仅在受控浏览器网络响应内替换 handleResize,不修改原站;只记录状态与更新次数,不测量性能收益。",
  "runs": [
    {
      "patched": false,
      "originalHash": "22d84ad71517db20c780b71753b612c62cda915737e9b00fc14090f807019be3",
      "replacementCount": 0,
      "before": [
        {
          "uid": 58,
          "component": "ByteTooltip",
          "popperStatus": false,
          "display": "none",
          "hasInstance": true,
          "eventsEnabled": true
        },
        {
          "uid": 60,
          "component": "ByteTooltip",
          "popperStatus": false,
          "display": "none",
          "hasInstance": true,
          "eventsEnabled": true
        },
        {
          "uid": 90,
          "component": "BytePopover",
          "popperStatus": false,
          "display": "none",
          "hasInstance": true,
          "eventsEnabled": true
        },
        {
          "uid": 106,
          "component": "BytePopover",
          "popperStatus": false,
          "display": "none",
          "hasInstance": true,
          "eventsEnabled": true
        },
        {
          "uid": 122,
          "component": "BytePopover",
          "popperStatus": false,
          "display": "none",
          "hasInstance": true,
          "eventsEnabled": true
        },
        {
          "uid": 138,
          "component": "BytePopover",
          "popperStatus": false,
          "display": "none",
          "hasInstance": true,
          "eventsEnabled": true
        },
        {
          "uid": 154,
          "component": "BytePopover",
          "popperStatus": false,
          "display": "none",
          "hasInstance": true,
          "eventsEnabled": true
        }
      ],
      "scroll": {
        "events": 24,
        "updates": 168
      },
      "transition": {
        "component": "ByteTooltip",
        "open": {
          "popperStatus": true,
          "display": "block",
          "hasInstance": true,
          "eventsEnabled": true
        },
        "closed": {
          "popperStatus": false,
          "display": "none",
          "hasInstance": true,
          "eventsEnabled": true
        }
      },
      "log": {
        "resize": [
          {
            "at": 1471.5,
            "uid": 58,
            "popperStatus": false,
            "hadInstanceBeforeCallback": false,
            "display": "none",
            "width": 0,
            "height": 0
          },
          {
            "at": 1474,
            "uid": 60,
            "popperStatus": false,
            "hadInstanceBeforeCallback": false,
            "display": "none",
            "width": 0,
            "height": 0
          },
          {
            "at": 1671.800000000745,
            "uid": 90,
            "popperStatus": false,
            "hadInstanceBeforeCallback": false,
            "display": "none",
            "width": 0,
            "height": 0
          },
          {
            "at": 1673.199999999255,
            "uid": 106,
            "popperStatus": false,
            "hadInstanceBeforeCallback": false,
            "display": "none",
            "width": 0,
            "height": 0
          },
          {
            "at": 1673.8999999985099,
            "uid": 122,
            "popperStatus": false,
            "hadInstanceBeforeCallback": false,
            "display": "none",
            "width": 0,
            "height": 0
          },
          {
            "at": 1674.3999999985099,
            "uid": 138,
            "popperStatus": false,
            "hadInstanceBeforeCallback": false,
            "display": "none",
            "width": 0,
            "height": 0
          },
          {
            "at": 1675.199999999255,
            "uid": 154,
            "popperStatus": false,
            "hadInstanceBeforeCallback": false,
            "display": "none",
            "width": 0,
            "height": 0
          },
          {
            "at": 6244.300000000745,
            "uid": 58,
            "popperStatus": true,
            "hadInstanceBeforeCallback": true,
            "display": "block",
            "width": 204,
            "height": 20
          },
          {
            "at": 6853,
            "uid": 58,
            "popperStatus": false,
            "hadInstanceBeforeCallback": false,
            "display": "none",
            "width": 0,
            "height": 0
          }
        ],
        "actualScrollEvents": 24,
        "updates": 170
      },
      "errors": []
    },
    {
      "patched": true,
      "originalHash": "22d84ad71517db20c780b71753b612c62cda915737e9b00fc14090f807019be3",
      "replacementCount": 1,
      "before": [
        {
          "uid": 58,
          "component": "ByteTooltip",
          "popperStatus": false,
          "display": "none",
          "hasInstance": false,
          "eventsEnabled": null
        },
        {
          "uid": 60,
          "component": "ByteTooltip",
          "popperStatus": false,
          "display": "none",
          "hasInstance": false,
          "eventsEnabled": null
        },
        {
          "uid": 90,
          "component": "BytePopover",
          "popperStatus": false,
          "display": "none",
          "hasInstance": false,
          "eventsEnabled": null
        },
        {
          "uid": 106,
          "component": "BytePopover",
          "popperStatus": false,
          "display": "none",
          "hasInstance": false,
          "eventsEnabled": null
        },
        {
          "uid": 122,
          "component": "BytePopover",
          "popperStatus": false,
          "display": "none",
          "hasInstance": false,
          "eventsEnabled": null
        },
        {
          "uid": 138,
          "component": "BytePopover",
          "popperStatus": false,
          "display": "none",
          "hasInstance": false,
          "eventsEnabled": null
        },
        {
          "uid": 154,
          "component": "BytePopover",
          "popperStatus": false,
          "display": "none",
          "hasInstance": false,
          "eventsEnabled": null
        }
      ],
      "scroll": {
        "events": 24,
        "updates": 0
      },
      "transition": {
        "component": "ByteTooltip",
        "open": {
          "popperStatus": true,
          "display": "block",
          "hasInstance": true,
          "eventsEnabled": true
        },
        "closed": {
          "popperStatus": false,
          "display": "none",
          "hasInstance": false,
          "eventsEnabled": null
        }
      },
      "log": {
        "resize": [
          {
            "at": 1326.5999999977648,
            "uid": 58,
            "popperStatus": false,
            "hadInstanceBeforeCallback": false,
            "display": "none",
            "width": 0,
            "height": 0
          },
          {
            "at": 1327.0999999977648,
            "uid": 60,
            "popperStatus": false,
            "hadInstanceBeforeCallback": false,
            "display": "none",
            "width": 0,
            "height": 0
          },
          {
            "at": 1490.3999999985099,
            "uid": 90,
            "popperStatus": false,
            "hadInstanceBeforeCallback": false,
            "display": "none",
            "width": 0,
            "height": 0
          },
          {
            "at": 1490.5999999977648,
            "uid": 106,
            "popperStatus": false,
            "hadInstanceBeforeCallback": false,
            "display": "none",
            "width": 0,
            "height": 0
          },
          {
            "at": 1490.5999999977648,
            "uid": 122,
            "popperStatus": false,
            "hadInstanceBeforeCallback": false,
            "display": "none",
            "width": 0,
            "height": 0
          },
          {
            "at": 1490.7999999970198,
            "uid": 138,
            "popperStatus": false,
            "hadInstanceBeforeCallback": false,
            "display": "none",
            "width": 0,
            "height": 0
          },
          {
            "at": 1490.8999999985099,
            "uid": 154,
            "popperStatus": false,
            "hadInstanceBeforeCallback": false,
            "display": "none",
            "width": 0,
            "height": 0
          },
          {
            "at": 6131.699999999255,
            "uid": 58,
            "popperStatus": true,
            "hadInstanceBeforeCallback": true,
            "display": "block",
            "width": 204,
            "height": 20
          },
          {
            "at": 6732.199999999255,
            "uid": 58,
            "popperStatus": false,
            "hadInstanceBeforeCallback": false,
            "display": "none",
            "width": 0,
            "height": 0
          }
        ],
        "actualScrollEvents": 24,
        "updates": 0
      },
      "errors": []
    }
  ]
}