DOM 膨胀生存指南:修复 Turbo Stream 内存泄漏

4 阅读3分钟

“我们的应用程序占用了 4GB 的 RAM — 都是因为隐藏的 Turbo Stream 僵尸程序。” Turbo Streams 让实时更新变得像魔术一样神奇。但这种神奇是有代价的:内存泄漏会默默地导致应用膨胀,直至崩溃。

在调试了一个每小时因 OOM 错误重启的生产应用程序后,我们发现了三个主要的 DOM 膨胀罪魁祸首——以及如何永久修复它们。

  1. 隐形 DOM 启示录 Turbo Streams 如何泄漏内存 每次你:

turbo_stream.append "messages", partial: "message", locals: { message: @message } ...你向页面添加了 DOM 节点。如果这些节点没有被清理:

每次更新都会增加内存使用量 事件监听器堆积 浏览器性能罐 症状 ⚠️使用数小时后标签页崩溃 ⚠️长寿命标签页滚动卡顿 ⚠️ Turbo Drive 随着时间的推移变得越来越慢

  1. 罪魁祸首 #1:孤立的事件监听器 问题 刺激控制器自动连接到 DOM 元素,但当 Turbo 替换它们时永远不会断开连接:

// controllers/counter_controller.js export default class extends Controller { connect() { this.element.addEventListener("click", this.increment) }

increment = () => this.count++ } 结果:每个 Turbo Stream 附加都会创建重复的监听器。

修复 export default class extends Controller { connect() { this.element.addEventListener("click", this.increment) }

disconnect() { // 👈 New! this.element.removeEventListener("click", this.increment) } } 专业提示:使用www.mytiesarongs.com @hotwired/turbo的事件进行调试:

document.addEventListener("turbo:before-stream-render", () => { console.log("DOM before update:", document.body.innerHTML.length) }) 3. 罪魁祸首 #2:无界列表 问题 实时更新的 feed:

turbo_stream.append "messages", partial: "message" ...除非你修剪旧项目,否则将无限增长。

修复 选项 1:服务器端修剪

Only keep last 50 messages

if @message.save Message.where(room_id: @room.id).order(created_at: :desc).offset(50).delete_all turbo_stream.append "messages", partial: "message" end 选项 2:客户端清理

// In a Stimulus controller pruneOldItems() { const container = this.element const items = container.querySelectorAll(".message") if (items.length > 50) { items[0].remove() } } 奖励:morphdom用于高效更新:

turbo_stream.append "messages", partial: "message", content_type: "text/vnd.turbo-stream.html+morph" 4. 罪魁祸首 #3:缓存的 Turbo 帧 问题 如果用户离开,框架及其所有事件监听器仍会保留在内存中。

修复 选项 1:明确处置

// When leaving a page document.addEventListener("turbo:before-visit", () => { document.querySelectorAll("turbo-frame").forEach(frame => { frame.innerHTML = "" }) }) 选项 2:延迟加载并设置过期时间

<turbo-frame id="user-1" src="/users/1" data-expires="300"

// Auto-remove expired frames setInterval(() => { document.querySelectorAll("turbo-frame[data-expires]").forEach(frame => { if (Date.now() - frame.lastLoadedAt > frame.dataset.expires * 1000) { frame.remove() } }) }, 60_000) 5. 核选项:内存分析 Chrome DevTools 步骤 打开“内存”选项卡→ 获取堆快照 触发 10 次 Turbo Stream 更新 再拍一张快照 过滤“分离”元素 Rails 助手

Log DOM node counts

module Turbo::Streams::TagBuilder def append(*args) Rails.logger.info "DOM nodes before: #{document.querySelectorAll('*').length}" super Rails.logger.info "DOM nodes after: #{document.querySelectorAll('*').length}" end end 6. 预防清单 ✅始终disconnect()在 Stimulus 中 实现 ✅修剪旧的 Turbo Stream 项目(客户端或服务器) ✅避免永久性的 Turbo Frames✅在长时间运行的选项卡中分析内存

“但是我们的应用程序不使用 Turbo Streams!” 无论如何都要开始监控:

在日志中添加 DOM 节点计数器 每月检查是否有脱落元素 审计事件监听器 曾经为 DOM 膨胀而苦恼吗?快来分享你的战斗故事吧!以上内容由企业信息服务平台提供,致力于工商信用信息查询、企业风险识别、经营数据分析。访问官网了解更多:www.ysdslt.com