惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 聂微东
W
WeLiveSecurity
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
The Blog of Author Tim Ferriss
博客园 - Franky
IT之家
IT之家
博客园_首页
I
Intezer
罗磊的独立博客
有赞技术团队
有赞技术团队
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Schneier on Security
GbyAI
GbyAI
人人都是产品经理
人人都是产品经理
V
V2EX
V
Visual Studio Blog
A
Arctic Wolf
Y
Y Combinator Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
C
Cybersecurity and Infrastructure Security Agency CISA
M
MIT News - Artificial intelligence
T
Tailwind CSS Blog
G
Google Developers Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
H
Help Net Security
Recent Announcements
Recent Announcements
量子位
Simon Willison's Weblog
Simon Willison's Weblog
D
DataBreaches.Net
博客园 - 叶小钗
宝玉的分享
宝玉的分享
AWS News Blog
AWS News Blog
P
Privacy International News Feed
A
About on SuperTechFans
Microsoft Azure Blog
Microsoft Azure Blog
T
The Exploit Database - CXSecurity.com
The Cloudflare Blog
雷峰网
雷峰网
The GitHub Blog
The GitHub Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
P
Privacy & Cybersecurity Law Blog
Security Latest
Security Latest
L
LINUX DO - 热门话题
T
Tor Project blog
The Register - Security
The Register - Security
C
Cyber Attacks, Cyber Crime and Cyber Security
Apple Machine Learning Research
Apple Machine Learning Research
大猫的无限游戏
大猫的无限游戏
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Threat Research - Cisco Blogs

BlogFinder

日常漫步 Vol.24 之漫步前山河 - 雅余 周报 #1-聊聊本周的收获 - Edwin's Blog 我的OpenCode必装插件与Skill Write Something 掌中之物未必在掌握之中 · CRIVU PiliNara,一个更顺手的 PiliPlus 分支 「NekoEcho」:做一个必有回响的猫娘主题博客 2026-05 书影音总结 简化博客主题 - 安迪 你要加油呐 我第一次发布 npm 包 拾花小记#45:中考前的二三事 – 小改学习志 黛西花园5月游 #18 枇杷又熟了的五月月报 一些奇奇怪怪的需求?word仿方正书版的几个小操作 - Xiobb's Blog 0419 御温泉之旅 修复了一些bug,网站基本上趋于稳定了 - 新锐博客 又回到四十年前 如何定义成功 迷鹿屋2026已重新上线 科技冰火两重天+一周回顾 ${title} 热度退了,我反而用得更深了-咕咚同学 我到底该不该换个域名? 随身WIFI折腾记 - 安迪 博客撰写体验提升——hexo pro插件 为什么不用相机把屏幕上的接关密码拍下来? 国清寺与天台山 – Ouroboros ★★★★☆《挽救计划》——久违的经济上行感 - Davidの3号基地 删除右键“打开方式”里多余选项 第三周刊_No.53|一切都会被支付两次 安卓APP通话记录与录音上传踩坑记录 - 子舒的博客 天量下跌 inBox 笔记 2.3.8,把工具栏交给了你-咕咚同学 我把小龙虾搬到了微信-咕咚同学 安好 - 响石潭 Compound Engineering Plugin:让每个工程单元都比上一个更容易 MOSS-TTS Family:开源高质量语音与声音生成模型家族深度解析 Crawl4AI:专为 LLM 设计的开源 Web 爬虫与数据抓取工具 Build Your Own X:从零实现你最喜欢的技术——程序员进阶的终极资源清单 Anthropic Skills:用文件夹教 Claude 专业技能的开源框架 1年的去月球(下) - 梅之夏 欢迎回来。 简单讲讲 ASN.1 与 OID DTV - 直播聚合客户端 5.22-5.27 – 不兴江 还没去过鸭川 – 不兴江 张晶晶同学三刷林志颖 关于我 – 不兴江 爱与嫉妒 – 不兴江 港股被持续做空 备案码花了四百块-咕咚同学 一句话生成封面:我给公众号做了4种风格的AI封面生成技能 「官」方認證 再谈费曼学习法 2026-05-28T00:34:11+08:00 2026-05-28T00:28:45+08:00 离谱的英语学习指南:基于AI的英语进阶系统方法论 iii:零集成架构的后端统一运行时 Claude Code Harness:让 Claude Code 工作有迹可循的工程化框架 Heretic:全自动移除大语言模型审查机制的开源工具 MarkItDown:微软开源的万能文档转 Markdown 利器 Harness:让 Claude Code 秒变多智能体协作工厂 这段时间尽折腾AI Agent了,确实极大地提高了效率 近期动态:两个新站点正式上线啦 误判解除!zhouayuan.com 腾讯安全申诉成功 - 周阿源|玩具设计・插画日常・生活随笔 Ralph:让 AI 编码工具自主循环跑完所有 PRD 任务的量产神器 全都违法 – 个人工作记录 关于zhouayuan.com被误判 “含违规信息” 的说明与申诉记录 - 周阿源|玩具设计・插画日常・生活随笔 小米 MiMo v2.5 Pro 白嫖 最大的人间清醒,兜里有钱,但是不花。 夜晚靓歌(12):于文文现场solo - 王志勇的Blog 今日插画:风扬起的倔强 - 周阿源|玩具设计・插画日常・生活随笔 回门习俗 独立网卡 - 忘记了回忆 500亿入股人工智能企业 从命令行到桌面智能体-咕咚同学 第一性原理读书笔记 行者微评论223-加班の守株待兔-博客|政治与时事-风雨行者 ZOZO开源物理接触求解器:GPU加速的可扩展仿真引擎 OpenStock:开源股票市场交易平台技术深度解析 MoneyPrinterTurbo:基于AI的全自动短视频生成工具深度解析 Claude-Mem:为 Claude Code 构建的持久化记忆压缩系统 Twenty:可代码化定制的企业级开源 CRM 平台技术深度解析 2026-05-26T22:59:17+08:00 企业级开源大模型部署平台 GPUStack 实战教程 1年的去月球(上) - 梅之夏 Sevalla - 静态网站托管服务 不用翻墙、不用注册、不用月费,普通人也能用上 Claude Code 装修灯具要注意⚠️ 黄梅天先锋 - 游子微博 公安备案顺利办结,站点备案全部完成 - 周阿源|玩具设计・插画日常・生活随笔 第三次兑换天猫超市卡了宗宗酱-三维狐少儿编程 Don't think, feel. - Rolen's Blog 人这一辈子,到底图个什么 博客迁移 - Edwin's Blog 情感赛道写作模板 再现本轮行情的典型特征 裁员与平常心-咕咚同学 别让“偷懒”,成为隐私泄露的破绽
HCLonely Blog - 记一次hexo-bilibili-bangumi分时函数渲染优化
HCLonely · 2026-06-15 · via BlogFinder

本文最后更新于天前,内容可能已不再适用!

在前端页面里,长列表渲染是一个很常见的性能问题。数据量不大时,直接map生成 HTML 再一次性插入 DOM 通常没有明显问题;但当列表变长、模板渲染逻辑变复杂、图片和元信息较多时,这种同步渲染方式就容易占满主线程,导致页面切换、滚动和点击出现明显卡顿。

本文记录一次对hexo-bilibili-bangumi追番页面分页渲染的优化:把原本一次性同步完成的模板渲染,改成在浏览器空闲帧中分批执行,从而降低单帧压力,让页面先保持可交互。

背景

追番页面会先渲染每个分类前 10 条数据,剩余数据通过bangumis.json异步加载。旧实现大致是这样的:

const html = {
  wantWatch: data.wantWatch.slice(10).map((item) => renderItem(item)).join('\n'),
  watching: data.watching.slice(10).map((item) => renderItem(item)).join('\n'),
  watched: data.watched.slice(10).map((item) => renderItem(item)).join('\n')
};

document.querySelectorAll('#bangumi-item1>.bangumi-pagination')[0].insertAdjacentHTML('beforeBegin', html.wantWatch);
document.querySelectorAll('#bangumi-item2>.bangumi-pagination')[0].insertAdjacentHTML('beforeBegin', html.watching);
document.querySelectorAll('#bangumi-item3>.bangumi-pagination')[0].insertAdjacentHTML('beforeBegin', html.watched);

这段代码的问题不在于写法复杂,而在于它把三类数据的模板渲染集中在一个任务里完成。浏览器主线程在执行这段 JavaScript 时,不能同时处理用户输入、样式计算、布局和绘制。如果数据量增加,单次任务耗时变长,就会出现掉帧和交互延迟。

对于博客页面来说,用户最直接的感受不是“渲染总耗时是多少”,而是“页面是不是能立刻响应”。因此优化目标不是把所有 HTML 更快地一次性生成出来,而是把大任务拆小,让浏览器有机会在任务之间处理渲染和输入。

原理

浏览器页面的 JavaScript、样式计算、布局、绘制和用户输入处理大多运行在主线程上。如果一个 JavaScript 任务长时间不结束,浏览器就没有机会进入下一帧,也就无法及时响应滚动、点击和视觉更新。

分时函数的核心思想是:

  1. 把一个大任务拆成多个小任务。
  2. 每次只执行一部分工作。
  3. 当前帧还有空闲时间时多做一点,没有空闲时间时让出主线程。
  4. 下一次空闲时继续处理剩余任务。

浏览器提供了requestIdleCallback,它会在主线程空闲时执行回调。回调参数里的deadline.timeRemaining()可以告诉我们当前空闲周期大概还剩多少时间。利用这个信息,可以把长列表渲染拆成多批。

不过requestIdleCallback并不是所有环境都支持,因此实现时还需要准备一个降级方案:如果浏览器不支持,就使用setTimeout延后执行。这样虽然不能精确感知空闲时间,但仍然可以避免在一个同步任务中渲染全部内容。

方法实现

这次优化拆成三步。

1. 封装空闲调度函数

先封装一个runWhenIdle,优先使用requestIdleCallback,否则退化到setTimeout

function runWhenIdle(callback) {
  if (typeof requestIdleCallback === 'function') {
    requestIdleCallback(callback);
    return;
  }
  setTimeout(() => {
    callback({
      timeRemaining: () => 0
    });
  }, 16);
}

这里的降级实现给了一个timeRemaining()为 0 的 deadline。后面的批处理逻辑会保证即使没有剩余时间,每一轮也至少处理一条数据,避免任务永远无法推进。

2. 复用编译后的模板函数

原来每条数据都直接调用pug.render。如果运行时支持pug.compile,可以先把模板编译成渲染函数,然后每条数据复用这个函数:

function createBangumiPageRenderer() {
  if (hexoBilibiliBangumiOptions.pug && typeof hexoBilibiliBangumiOptions.pug.compile === 'function') {
    const render = hexoBilibiliBangumiOptions.pug.compile(hexoBilibiliBangumiOptions.pugTemplate);
    return function hexoBilibiliBangumiRenderPage(item) {
      return render({
        item,
        ...hexoBilibiliBangumiOptions.pugOptions
      });
    };
  }
  return function hexoBilibiliBangumiRenderPage(item) {
    return hexoBilibiliBangumiOptions.pug.render(hexoBilibiliBangumiOptions.pugTemplate, {
      item,
      ...hexoBilibiliBangumiOptions.pugOptions
    });
  };
}

这一步不是分时渲染的必要条件,但它能减少每条数据的重复开销。长列表优化通常要同时关注两个方向:减少总计算量,以及避免单次计算阻塞太久。

3. 在空闲帧中分批渲染

核心批处理逻辑如下:

function renderItemsInIdle(items, renderPage, onComplete) {
  const html = [];
  let index = 0;

  function renderBatch(deadline) {
    let renderedInFrame = false;
    while (index < items.length && (!renderedInFrame || deadline.timeRemaining() > 0)) {
      html.push(renderPage(items[index]));
      index++;
      renderedInFrame = true;
    }

    if (index < items.length) {
      runWhenIdle(renderBatch);
      return;
    }

    onComplete(html.join('\n'));
  }

  runWhenIdle(renderBatch);
}

这里有一个细节:循环条件不是简单的deadline.timeRemaining() > 0,而是:

!renderedInFrame || deadline.timeRemaining() > 0

这样可以保证每个空闲回调至少渲染一条数据。否则在降级方案里timeRemaining()一直是 0,任务就会被不断重新调度,却不会真正处理任何条目。

最后,把三个分类组织成任务队列,按顺序分批渲染并插入到对应分页按钮之前:

function renderTasksInIdle(tasks) {
  const renderPage = createBangumiPageRenderer();
  let taskIndex = 0;

  function runNextTask() {
    if (taskIndex >= tasks.length) return;

    const task = tasks[taskIndex];
    taskIndex++;

    renderItemsInIdle(task.items, renderPage, (html) => {
      document.querySelectorAll(task.selector)[0].insertAdjacentHTML('beforeBegin', html);
      runNextTask();
    });
  }

  runNextTask();
}

调用时只需要传入数据和目标选择器:

renderTasksInIdle([
  {
    items: data.wantWatch.slice(10),
    selector: '#bangumi-item1>.bangumi-pagination'
  },
  {
    items: data.watching.slice(10),
    selector: '#bangumi-item2>.bangumi-pagination'
  },
  {
    items: data.watched.slice(10),
    selector: '#bangumi-item3>.bangumi-pagination'
  }
]);

效果

优化前,bangumis.json加载完成后,页面会立刻同步执行三类列表的模板渲染。数据越多,单次任务越长,用户越容易感受到卡顿。

优化后,渲染被拆分到多个空闲帧中执行:

  • 首屏内容和标签切换逻辑不需要等待全部剩余数据渲染完成。
  • 浏览器可以在批次之间处理绘制和输入事件。
  • 单帧 JavaScript 执行时间降低,滚动和点击更不容易被长任务阻塞。
  • 支持requestIdleCallback的浏览器可以根据真实空闲时间动态多渲染几条;不支持时也能通过setTimeout分批推进。
  • Pug 模板优先编译后复用,减少重复解析模板的成本。

这种优化不会让所有内容“瞬间完成”,但会改善用户感知性能。对用户来说,页面能先响应、逐步补齐内容,通常比等待一个长任务全部执行完更自然。

适用场景

分时渲染适合以下情况:

  • 列表数据较多,但不要求所有内容立即同步展示。
  • 每条数据的渲染逻辑较重,例如模板渲染、格式化、复杂 DOM 字符串拼接。
  • 页面初始交互比完整内容一次性出现更重要。
  • 不方便引入虚拟列表,但希望降低长任务阻塞。

如果是后台管理系统里的超大表格,虚拟列表可能是更彻底的方案;如果是博客、文章页、追番页这类静态内容为主的场景,分时渲染的改动更小,收益也比较直接。

注意事项

分时渲染不是银弹,实现时需要注意几个边界:

  1. 每个批次至少处理一项,避免低空闲时间或降级方案下任务无法推进。
  2. 插入 DOM 的时机要稳定。本文的实现是在某个分类全部渲染完成后再插入,避免一个分类的内容被频繁分段插入造成布局抖动。
  3. 如果用户可能在渲染未完成时切换页面或销毁容器,需要增加取消机制或容器存在性判断。
  4. 如果列表极长,单纯把 HTML 存在数组中最后一次性插入仍可能占用较多内存,可以进一步改成每 N 条插入一次。
  5. requestIdleCallback适合低优先级任务,不适合用户点击后必须立即完成的关键反馈。

总结

这次优化的关键不是换一个更复杂的框架,而是把“同步做完所有事”的思路改成“浏览器空闲时分批做”。对长列表、模板渲染和博客插件这类场景来说,分时函数是一个成本低、侵入小、效果明确的优化手段。

最终实现保留了原有数据结构和 DOM 插入位置,只替换了渲染调度方式:数据仍然来自bangumis.json,模板仍然使用 Pug,展示结果保持一致,但渲染过程对主线程更友好。