本网站不收集任何访问者的行为与信息,不做任何商业运作,仅仅为个人使用。闽ICP备17026139号-1

深入理解 Navigation API

一、 设计哲学 (The "Why")

  1. 将导航的“语义”交还浏览器: 传统 SPA 路由(基于 history.pushState/replaceState)本质上是在“欺骗”浏览器。我们只是改变了 URL 和一些历史记录状态,但浏览器本身并不知道一次真正的“导航”正在发生。Navigation API 的核心哲学是让浏览器真正理解并参与到 SPA 的导航过程中。它不再仅仅是被动地记录历史条目,而是主动地管理导航生命周期。
  2. 以用户意图为中心,而非技术实现: pushState 是一个低级、命令式的操作。Navigation API 则更加声明式和事件驱动。它关注的是用户发起的导航意图(如点击链接、前进/后退按钮)或程序触发的导航请求 (navigation.navigate()),并围绕这个意图提供了一套完整的生命周期事件 (navigate, navigatesuccess, navigateerror, currententrychange)。这使得开发者可以更好地响应和控制导航流程。
  3. 标准化与健壮性: 在 Navigation API 出现之前,每个前端框架都需要在 history API 之上构建自己复杂的路由管理逻辑,包括处理并发导航、滚动恢复、焦点管理、可访问性(ARIA Live Regions 通知等)。这导致了实现碎片化和潜在的健壮性问题。Navigation API 旨在提供一个标准化的、更健壮的底层基础,让框架和开发者能在此之上构建更可靠、更一致的用户体验。
  4. 拥抱异步本质: 现代 Web 应用的导航往往涉及异步操作(代码分割加载、数据获取)。history API 对此无能为力。Navigation API 通过 NavigateEvent.intercept(handler) 明确地支持了异步导航处理,允许开发者在导航真正完成(URL 变更、DOM 更新)之前执行异步任务,并且可以优雅地处理成功、失败或取消。

“导航生命周期”的完整定义:

“导航生命周期”在 Navigation API 的语境下,指的是从用户或程序发起导航意图开始,到导航最终完成(成功或失败),并且浏览器状态(URL、历史记录、DOM)更新为止的整个过程,以及期间由浏览器管理和触发的一系列事件和状态。

其关键阶段和事件包括:

  1. 导航触发 (Initiation):

    1. 用户行为:点击链接 (<a>)、提交表单(如果未被阻止且目标是当前标签页)、点击浏览器前进/后退/刷新按钮。
    2. 程序化调用:navigation.navigate(), navigation.reload(), navigation.back(), navigation.forward(), navigation.traverseTo()
  2. navigate 事件分发 (Intent & Interception):

    1. 浏览器捕获导航意图,在任何实际状态改变(URL、History)之前,在 window.navigation 上触发 navigate 事件。
    2. 该事件 (NavigateEvent) 提供了导航的详细信息(目标 URL、状态、是否用户触发 userInitiated 等),以及控制导航的方法:
    3. canIntercept: 是否可以调用 intercept() (通常对于跨域导航等情况为 false)。
    4. preventDefault(): 同步取消导航。
    5. intercept({ handler }): 核心机制。声明应用将接管导航处理(通常是异步的,如加载数据、更新 DOM)。浏览器会等待 handler 这个 async 函数关联的 Promise 完成。handler 会接收到一个 AbortSignal (event.signal) 用于处理后续导航触发的取消。
  3. 处理阶段 (Processing - if intercepted):

    1. 如果调用了 intercept(options),浏览器等待 options.handler 的 Promise。
    2. 开发者在此 handler 中执行异步操作(fetch 数据、懒加载模块、渲染视图)。
    3. 如果在此期间发生新的导航,之前 intercepthandler 关联的 AbortSignal 会被触发 (aborted),开发者应中止当前处理并让 handler 的 Promise reject 或快速 resolve。
  4. 提交阶段 (Commitment):

    1. 如果未拦截或拦截成功完成 (handler Promise resolves):
    2. 浏览器更新 URL。
    3. 更新 navigation.currentEntry
    4. 将新的或更新后的 NavigationHistoryEntry 添加/更新到历史记录堆栈中。
    5. 触发 navigatesuccess 事件,表示导航逻辑成功完成。
    6. 更新 DOM(如果是由 intercept handler 负责的)。
    7. 触发 currententrychange 事件,因为 currentEntry 已改变。
    8. 浏览器可能执行默认行为,如滚动恢复、焦点管理、触发无障碍通知。
    9. 如果拦截失败 (handler Promise rejects) 或导航被阻止:
    10. URL 和历史记录发生改变。
    11. 触发 navigateerror 事件,表示导航逻辑失败。currententrychange 触发。

这个完整的生命周期由浏览器原生管理,提供了比 popstate + pushState 更为健壮和可预测的控制流,特别是对于异步操作和并发导航的处理。

二、 解决问题的思路 (The "How")

  1. 显式导航生命周期管理:
    1. navigate 事件: 核心入口。导航意图发生时触发。提供信息并允许控制:
      1. 检查目标 (event.destination.url, event.destination.getState())。
      2. 同步取消 (event.preventDefault())。
      3. **关键:**通过 event.intercept({ handler }) 声明接管导航,执行异步逻辑。handler (async function) 的 Promise 决定导航结果。浏览器等待此 Promise。
    2. navigatesuccess / navigateerror 事件:intercepthandler 成功 resolve 或 reject 后(或无拦截时导航完成后)触发,提供明确的完成/失败信号。
    3. currententrychange 事件:navigation.currentEntry 发生变化时(导航成功完成、调用 updateCurrentEntry())触发,响应当前历史条目状态更新。
  2. 更精细的历史记录管理:
    1. NavigationHistoryEntry 对象: 结构化历史条目,含 key (唯一标识), id (同文档唯一), url, index, 和 getState() 获取关联状态。
    2. navigation.entries() 访问整个历史堆栈(只读视图)。
    3. navigation.updateCurrentEntry({ state }) 在不触发导航的情况下,更新当前历史条目的状态(使用结构化克隆存储)。用于保存页面临时状态(表单、滚动位置等)。
    4. navigation.traverseTo(key) 直接导航到历史记录中指定 key 的条目,而非仅相对前进/后退。会丢弃后续历史。
  3. 原生处理常见 SPA 痛点:
    1. 滚动恢复: API 设计为浏览器实现更可靠的自动滚动恢复提供了基础(具体策略可能由浏览器决定)。开发者也可在 navigate({scroll:"after-transition"|"manual"})navigatesuccess 中手动处理,或利用 history.scrollRestoration
    2. 焦点管理: 导航成功后,浏览器可以应用更智能的默认焦点行为(如 autofocus 或聚焦 <body>)。开发者也可在 navigate({focusReset:"after-transition"|"manual"})navigatesuccess 中手动管理。
    3. 可访问性 (A11y): 标准化事件(尤其 navigatesuccess)为 ARIA live regions 或平台辅助技术提供了宣告页面转换的可靠时机。浏览器自身也可能利用此信号发出通知。
    4. 并发处理(关键优势): API 内建处理快速连续导航的机制。后续导航意图会通过 AbortSignal (event.signal) 中止正在进行的 intercept handler,确保响应最新意图,防止竞态条件。

      当一个导航被 event.intercept({ handler }) 拦截并且 handler 的 Promise 正在进行中时,如果此时发生了另一次导航(用户点击、代码调用 navigate() 等),提案和规范对此有明确的处理机制:不是排队等待,而是后者取代前者 (Superseding)。具体流程如下:

      1. 导航 A 触发: navigate 事件 A 触发。
      2. 拦截 A: 代码调用 eventA.intercept({ handler: handlerA })handlerA (async function) 开始执行。
      3. 导航 B 触发 (在 handlerA 完成前): 用户点击链接或代码调用 navigation.navigate() 触发了新的导航 B。
      4. navigate 事件: 浏览器立即为导航 B 触发一个新的 navigate 事件 B。
      5. 中止信号触发: 关键点:事件 A 关联的 NavigateEvent (eventA) 上的 signal (eventA.signal) 属性(这是一个 AbortSignal)会立即被浏览器触发 abort
      6. handler 处理中止:
        1. handlerA 内部的代码应该监听这个 eventA.signal。可以通过 eventA.signal.aborted 检查状态,或者使用 eventA.signal.addEventListener('abort', ...)
        2. 当检测到信号中止时,handlerA 应该尽快停止其工作(例如,中止进行中的 fetch 请求,取消定时器,停止 DOM 更新)并让其返回的 Promise reject (通常使用一个表示中止的特定错误,如 DOMException('AbortError')) 或快速 resolve。
      7. 旧导航结果: 由于 handlerA 的 Promise 通常会因中止而 reject (或者即使 resolve,浏览器也知道它被中止了),导航 A 不会进入“提交阶段”。它不会更新 URL,不会触发 navigatesuccess。它可能会(也应该会)触发 navigateerror (如果 Promise reject 了),但这代表的是被中止的导航 A 的失败,而不是导航 B 的状态。浏览器实质上抛弃 (discards) 了被中止的导航 A 的后续流程。
      8. 新导航 B 继续: 导航 B 的 navigate 事件 (eventB) 现在正常处理。它可以被 preventDefault(), 或者也被 intercept() 等。它接管了导航流程。

state vs. searchParams 的使用时机

  1. searchParams (URL 查询参数):

    1. 用途: 用于表示资源状态的关键参数,这些状态应该反映在 URL 中。它们定义了“你正在看什么”。
    2. 特点:
      1. 可见、可编辑(用户可以直接修改 URL)。
      2. 可分享、可收藏。
      3. 通常是字符串键值对。
      4. 搜索引擎可索引。
      5. 改变 searchParams 通常意味着请求不同或过滤后的数据子集。
    3. 使用场景:
      1. 分页 (?page=2)
      2. 排序 (?sort=price_desc)
      3. 过滤 (?category=electronics&brand=xyz)
      4. 搜索词 (?q=navigation+api)
      5. 选项卡或视图切换(如果每个视图代表根本不同的内容切片,如 ?tab=details)。
      6. 任何需要持久化可链接地表示应用内容状态的情况。
  2. navigation.currentEntry.getState() / navigation.navigate(url, { state: ... }) / navigation.updateCurrentEntry({ state: ... }):

    1. 用途: 用于存储与特定历史记录条目相关联的、非 URL 可见的应用状态。它更多是关于“当你访问这个 URL 时,当时的 UI 处于什么临时状态”。
    2. 特点:
      1. 用户不可见,不影响 URL 字符串。
      2. 不可直接分享或收藏(分享 URL 不会带上 state)。
      3. 可以存储更复杂的结构化数据(只要满足结构化克隆算法)。
      4. 与特定历史记录条目 (NavigationHistoryEntry) 绑定,通过 back/forward/traverseTo 导航回该条目时,可以恢复。
      5. 修改 state (通过 updateCurrentEntry) 会触发完整的导航生命周期(不触发 navigate 事件),只会触发 currententrychange
    3. 使用场景:
      1. 滚动位置恢复: 保存页面的精确滚动位置,以便返回时恢复 (虽然浏览器可能提供默认行为,但 state 可用于更精细的控制)。
      2. 临时 UI 状态: 例如,模态框是否打开、某个 <details> 元素是否展开、手风琴面板状态。
      3. 部分填写的表单数据: 用户填写了一半表单,导航离开又回来,可以恢复输入。
      4. 列表中的高亮项: 用户在列表页点击一项进入详情,返回时希望之前点击的项仍然高亮。
      5. 特定于访问的状态: 需要在同一次会话中、通过历史导航恢复的、与 URL 内容本身不直接相关的界面状态。
  3. state 是否被克隆存储?

    1. 是的。传递给 navigate(), updateCurrentEntry() 或最初与 pushState/replaceState 关联的 state 对象,会被浏览器使用结构化克隆算法 (Structured Clone Algorithm) 进行克隆,然后存储。
    2. Implications:
      1. 无法存储: 函数、DOM 节点、Error 对象、某些类的实例(除非它们特殊处理过)、带有循环引用的对象。
      2. 可以存储: 原始类型、普通对象、数组、Date, RegExp, Blob, File, FileList, ArrayBuffer, ImageData, Map, Set 等。
      3. 性能考虑:存储非常大的对象可能会影响性能。
      4. 由于是克隆,后续修改原始对象不会影响存储的状态,反之亦然。

选择依据: 问自己:这个状态是否需要体现在 URL 中?是否需要用户能够收藏或分享这个状态?这个状态是定义了资源内容本身,还是仅仅是用户与该资源交互时的临时界面状态?前者用 searchParams,后者用 state。它们可以并存。

三、 背后的故事与动机 (The "Context")

Navigation API (曾用名 App History API) 的诞生源于 Web 开发者社区和浏览器供应商多年来对 SPA 路由现状的普遍不满。history API 设计于 Web 早期,并未预见到现代复杂单页应用的导航需求。框架作者们(如 React Router, Vue Router, Angular Router)不得不花费大量精力来弥补底层 API 的不足。

你可以通过提案链接了解到这个提案发展的过程:github.com/WICG/navigation-api

也可以通过这个视频快速地了解其背后的故事:The history API is dead. Long live the navigation API | HTTP 203 YouTube

四、 与其它提案的联动 (Synergies)

  1. View Transitions API (依然是黄金搭档,但集成方式不同):

    1. Navigation API 定义导航逻辑和时机,View Transitions API 处理视觉状态间的平滑过渡

    2. 集成方式: 在 Navigation API 的 navigate 事件监听器中:

    3. 调用 event.intercept({ handler }) 来接管导航。

    4. handler 这个异步函数内部,使用 document.startViewTransition() 包裹你的 DOM 更新和可能的数据获取逻辑。

      navigation.addEventListener("navigate", (event) => {
        if (!event.canIntercept) {
          return;
        }
        if (!document.startViewTransition) {
          // 如果浏览器不支持 View Transitions
          event.intercept({
            handler: async () => {
              /* 直接更新DOM */
            },
          });
          return;
        }
      
        // 使用 View Transitions
        event.intercept({
          async handler() {
            // ★ 在 intercept 的 handler 内部调用 startViewTransition
            const transition = document.startViewTransition(async () => {
              // 异步加载数据(如果需要)
              const data = await fetchData(event.destination.url);
              // 更新 DOM
              updateTheDOM(data);
            });
      
            // 可以选择等待过渡动画完成 (transition.finished)
            // 或仅等待伪元素创建/DOM更新完成 (transition.updateCallbackDone)
            // 或甚至不等待,取决于你的逻辑需求
            try {
              await transition.updateCallbackDone; // 至少等 DOM 更新完成
            } catch (e) {
              // 处理 DOM 更新或数据获取中的错误
              console.error("DOM update failed:", e);
              throw e; // 重新抛出,让 navigateerror 触发
            }
          },
        });
      });
      
  2. 其它可能相关的领域 (间接):

    1. Speculation Rules API: 标准化的导航流程有助于更准确地触发 prefetch。
      1. 当 Navigation API 的 navigate 事件触发时,如果 Speculation Rules 已经成功 prefetch 了所需的数据或代码块,那么在 intercept handler 中执行的相应 fetch 或动态 import() 调用会显著加快,从而缩短导航的感知时间。
      2. <link rel="prefetch"> 也可以达到预取效果,但 Speculation Rules 提供了更现代、更灵活、可能更强大的机制来做同样的事情,尤其是在动态识别和管理预取目标方面。
      3. eagerness 控制 (虽然此特性还在演进): 意图是允许开发者提示预取的紧迫性(例如,eager 可能意味着在用户悬停时就开始)。(注意:eagerness 的具体实现和行为仍在讨论和标准化中,浏览器可能有自己的策略)。
      4. 参考文章 对 Speculation Rules API 的改进
    2. Performance Timeline / Reporting API: navigatesuccessnavigateerror 为性能监控和错误报告提供了更精确的时间点和上下文。

五、 关于 Navigation API Polyfill

为 Navigation API 编写一个功能完善的 Polyfill 挑战巨大,因为它试图在用户空间模拟浏览器内核级的导航管理(所以完全模拟是不现实的,只能说尽量,而且用户使用的时候要小心一些边缘情况)。

目前市面上比较成熟的 Polyfill 仓库是 github.com/virtualstate/navigation (esm-bundle 差不多需要 105kb)

  1. Polyfill 实现的核心难点 (普遍性挑战):

    模拟 Navigation API 的行为,尤其是在尝试复刻其所有功能时,会遇到一些根本性的限制,导致 Polyfill 的行为与原生 API 存在开发者和用户都能感知到的差异:

    1. 无法真正实现“事前”拦截浏览器历史导航 (popstate)

      1. 挑战: 这是最核心的差异之一。原生 navigate 事件在浏览器实际更改 URL 或历史记录之前触发,允许开发者通过 event.preventDefault() 完全取消导航,或通过 event.intercept() 在状态改变前执行异步逻辑。然而,Polyfill 赖以感知浏览器前进/后退操作的 popstate 事件,是在 URL 和历史指针已经改变之后才触发。
      2. 显著差异/局限性:
        1. 无法阻止 popstate 导航: Polyfill 无法在 popstate 触发时真正阻止浏览器历史状态的改变。它最多只能在事件触发后,尝试通过 history.pushStatehistory.replaceState 将状态“修正”回来,但这会导致地址栏 URL 短暂闪烁成目标 URL 再变回来,用户可以明显感知。
        2. 无法实现可靠的 popstate 前置校验: 开发者不能依赖 Polyfill 在用户点击后退按钮时,进行类似“您有未保存的更改,确定要离开吗?”的同步确认(因为状态已变)。原生 API 的 navigate 事件则可以完美支持此场景。
      3. 例子: 用户在表单页点击后退。使用原生 API,navigate 事件可以在 URL 变化前弹出确认框阻止导航。使用 Polyfill,popstate 触发时 URL 已变,Polyfill 尝试修正会引发 URL 闪烁,且阻止逻辑发生在状态改变之后。
    2. 无法控制浏览器原生 UI 和行为

      1. 挑战: Polyfill 运行在 JavaScript 用户空间,对浏览器本身的 UI 组件和底层行为控制力为零。
      2. 显著差异/局限性:
        1. 加载指示器: Polyfill 无法控制浏览器的标签页加载微调器(spinner)或进度条。在 intercept() 执行异步操作期间,浏览器不会像原生导航那样显示加载状态,除非开发者手动模拟一个加载指示器。用户可能会感觉应用“卡顿”而不是“正在加载”。
        2. 地址栏 URL 显示: 原生 API 在 intercept() 执行期间,地址栏通常会保持旧 URL,直到导航成功提交才更新。Polyfill 无法控制这一点,尤其是在 popstate 场景下,地址栏内容已经提前改变。
        3. 原生滚动恢复/焦点管理: Polyfill 无法改变浏览器对 history.scrollRestoration 的处理方式,也无法完全复刻原生导航后复杂的默认焦点管理逻辑(例如 autofocus 属性在导航后的行为)。开发者需要手动实现滚动和焦点逻辑,其效果可能与原生默认行为有细微但可感知的差异。
        4. 无障碍 (A11y) 通知: Polyfill 无法触发平台原生的导航成功/失败的无障碍通知。开发者必须手动更新 ARIA live regions 来宣告状态变化。
    3. 难以拦截或阻止某些导航触发方式

      1. 挑战: JavaScript 对某些浏览器内置的导航机制无能为力。
      2. 显著差异/局限性:
        1. 直接 location API 调用:location.assign(), location.replace(), location.href = ...。Polyfill 无法在这些调用实际执行并导致页面跳转或重载之前拦截它们。这是 Polyfill 的一个硬性限制,“做不到”阻止这类导航。

          web-worker 中的 location 的定义是藏在原型链上的,所以可以覆写 self.location。 而 main-thread 中的 location 是直接锁定在 window.location 上,同时 location 本身的属性也都锁死在 location 自身的对象上,完全没有使用原型链,所以基本锁死了所有的修改的可能。

        2. 非 JS 触发的导航:<meta http-equiv="refresh"> 或用户通过浏览器扩展触发的导航,Polyfill 基本无法介入。
    4. 状态管理 (getState/state):跨页面加载的历史上下文恢复挑战

      1. 挑战: 尽管 history.state 支持结构化克隆且能在页面刷新后恢复当前条目的状态,但 Polyfill 面临的根本挑战在于恢复页面刷新前整个导航历史的上下文视图。页面重载会清除 Polyfill 在 JavaScript 内存中维护的内部历史表示(所有条目的 key, id, url, state 等信息)。为了在刷新后模拟 navigation.entries()navigation.traverseTo(key) 等 API,Polyfill 必须尝试恢复这些丢失的信息,通常采取以下两种策略,每种策略都有其固有的、显著的局限性:

      2. Polyfill 策略 A: 将整个 navigation.entries() 存入当前 history.state

        1. 挑战: 这会导致该 state 对象的大小极易触及并超过浏览器对单个 history.state 对象的大小限制(该限制因浏览器而异,通常在几百 KB 到若干 MB)。
        2. 显著差异/局限性:
          1. 强加的存储限制: Polyfill 迫使开发者不仅要限制每个历史条目的 state 大小,而且应用的历史记录深度navigation.entries() 的长度)本身也成为一个严格的限制因素。随着历史变长,history.state 会迅速达到上限,导致 Polyfill 无法保存新的历史信息或在刷新后恢复完整的上下文,行为变得不可靠或直接失败。这与原生 API(仅受单个条目大小限制,不受历史总长度的直接限制)的行为模式构成显著差异。
          2. 性能开销: 序列化和反序列化整个大型历史表示对象,在每次导航(更新 state)和页面加载(恢复 state)时都会带来额外的性能开销。
      3. Polyfill 策略 B: 使用外部存储 (sessionStorage 或者 IndexedDB) 持久化 Polyfill 历史表示

        1. 挑战 (使用 sessionStorage): 将 Polyfill 的内部历史表示序列化为字符串存入 sessionStorage
        2. 显著差异/局限性 (使用 sessionStorage):
          1. 类型限制与序列化复杂性: sessionStorage 仅能存储字符串。即使底层 history.state 支持结构化克隆,为了存入 sessionStorage,Polyfill 必须进行序列化。为了保持一致性,这里就需要额外引入类似 superjson 这样的库来做自定义的序列化反序列化。
          2. 更严格的大小限制: sessionStorage 的大小限制(通常 5-10MB)可能比浏览器对单个 history.state 的限制更严格,进一步压缩了 Polyfill 能管理的总历史状态空间。
          3. 同步读写性能: 在页面加载时同步读取和解析 sessionStorage 数据会阻塞主线程,影响启动性能。
        3. 挑战 (使用 IndexedDB): 将 Polyfill 的历史表示存入容量更大的 IndexedDB。
        4. 显著差异/局限性 (使用 IndexedDB):
          1. 同步/异步接口冲突: IndexedDB 是一个异步 API,而 Navigation API 的核心部分(如 navigation.entries(), navigation.currentEntry, entry.getState())是同步设计的。Polyfill 无法在调用这些同步方法时同步地从 IndexedDB 获取所需数据。这意味着:
            1. 刷新后首次调用这些同步 API 可能返回空、不完整或过时的数据,直到异步加载完成后状态才更新,这与原生 API 的即时可用性形成根本性差异
            2. 如果 Polyfill 强制使用异步接口的设计,一方面开发者会感知到 API 行为的不一致,另一方面,应用启动的速度会受到一些影响,甚至会影响启动的正确性。
    5. 并发导航处理中的可见状态风险 (非精确性导致的可感知问题)

      1. 挑战: 虽然时序的微小差异本身不易察觉,但 Polyfill 在模拟 AbortSignal 和管理并发状态时若不够健壮,可能导致可感知的副作用。
      2. 显著差异/局限性:
        1. 状态更新冲突: 如果 Polyfill 未能及时或完全中止前一个 intercept handler(尤其当 handler 代码未良好响应中止信号时),旧 handler 可能在后续导航开始处理后,仍然修改了 DOM 或应用状态,导致界面短暂显示错误内容或数据不一致,用户可能看到“闪烁”或错误的中间状态。
        2. 资源浪费: 未能中止的操作(如后台请求)会继续运行,消耗用户资源。
  2. 使用 Navigation API Polyfill 的关键注意事项 (基于显著差异)

    1. 弥补核心功能差距: 认识到 Polyfill 无法真正实现 popstate 的事前拦截(影响离开确认等场景),也无法控制原生 UI(加载指示器、滚动/焦点)或触发平台级无障碍通知。开发者必须手动实现这些缺失的 UI 反馈和辅助功能。同时,应规范使用 navigation.navigate(),避免 Polyfill 难拦截的 location API 调用。
    2. 在限制内管理状态与性能: 由于 Polyfill 跨页面加载恢复历史上下文的挑战(常依赖 sessionStorage),必须严格限制 state 中存储数据的复杂度和大小,并注意控制历史记录深度,以防超出存储限制或遭遇序列化问题。确保 intercept Handler 健壮地处理 AbortSignal 以应对并发。最后,优先条件加载 Polyfill,并进行性能评估