React useObjectUrl Hook:预览文件与 Blob,不留内存泄漏(2026)

用户往图片编辑器里上传了十几张图片,每一张都通过 URL.createObjectURL() 生成实时预览。会话持续了二十分钟,预览随着文件的增删不断切换,标签页的内存占用则一路攀升------因为每一次 createObjectURL() 调用都必须配对一次 revokeObjectURL() 调用,而这个配对关系要在重渲染、props 变化、组件卸载之间始终成立。大多数组件都没做对,而这个 bug 要等到有人把标签页开得够久、注意到风扇转起来了才会被发现。

useObjectUrl 就是那个一行代码的修复:把 FileBlobMediaSource 丢给它,拿回一个 URL 字符串,再也不用自己调用 revokeObjectURL。以下都是 @reactuses/core 的真实 API,TypeScript 优先。

手写版本,以及它在哪里泄漏

URL.createObjectURL() 是浏览器里支撑一切客户端文件预览的 API:传给它一个 BlobFile 就是其中一种),它会返回一个 blob: URL,可以直接塞进 <img src><video src>。问题出在另一端------它铸造出的每一个 URL 都会持续存活、持有底层数据的引用,直到你显式调用 URL.revokeObjectURL()。忘记调用,或者在错误的时机调用,这个 URL------以及它背后的内存------就永远不会被释放。

在 React 组件里,"错误的时机"很容易撞上:

tsx 复制代码
function FilePreview({ file }: { file?: File }) {
  const [url, setUrl] = useState<string>();

  useEffect(() => {
    if (!file) return;
    const next = URL.createObjectURL(file);
    setUrl(next);
    return () => URL.revokeObjectURL(next);
  }, [file]);

  return url ? <img src={url} /> : null;
}

这一版其实是对的------清理函数闭包捕获的是 next,而不是 state,所以它总能精确回收自己创建的那个 URL。但它"对"的方式,正是手写 effect 常见的那种脆弱:只要一次不小心的重构就会出问题。把 URL.createObjectURL 调用挪到 effect 外面、错误地做了 memo、或者从 state 里读 url 而不是本地变量 next,配对关系就会悄无声息地断裂。把这套模式复制到每一个需要预览文件的组件里------头像上传、聊天附件、图库缩略图------你就是在五个不同的地方维护着同一套脆弱的生命周期逻辑。

useObjectUrl ------ 创建与回收,都替你处理好

tsx 复制代码
import { useObjectUrl } from '@reactuses/core';

function FilePreview({ file }: { file?: File }) {
  const url = useObjectUrl(file);
  return url ? <img src={url} /> : null;
}

整个 hook 就这么多。签名是:

ts 复制代码
function useObjectUrl(object: Blob | MediaSource): string | undefined;

传入一个 Blob(或者它的子类型 File)或 MediaSource,拿回对应的 object URL------在来源存在之前则是 undefined。它内部就是一个 useEffect:来源存在时调用 URL.createObjectURL(),并返回一个调用 URL.revokeObjectURL() 的清理函数。它和手写版本的区别不在行为,而在于这套配对关系只存在于一个经过审查的地方,而不是每个组件各自重新推导一遍。

来源变化时,以及组件卸载时

回收会在两个真正重要的时机触发:

  • 来源变化。file prop 换成另一个 File,hook 会在铸造新 URL 之前先回收旧的------用户逐个点开附件列表时不会产生堆积。
  • 组件卸载。 关闭预览弹窗、跳转到别的页面、从列表里移除某一项------清理函数都会执行,URL 会被释放。不会留下什么等着一个根本不知道要去回收它的垃圾回收器。

第二种情况恰恰是手写代码最常漏掉的一种,因为它只会在真实使用中表现为缓慢的内存爬升,快速的手动测试根本发现不了。

真实使用场景

  • 文件输入预览。 在用户提交上传之前,把刚选中的图片、视频或 PDF 展示出来------这是最典型的场景。
  • Blob API 响应。 一个返回 Blob 的接口(生成的导出文件、带签名的资源、fetch().blob() 的结果)可以直接变成可下载的 <a href>,或者内联的 <img>/<video>,不需要先写到磁盘上。
  • Canvas 导出。 canvas.toBlob() 会产出一个 Blob;把它直接喂给 useObjectUrl,就能预览或提供下载用户刚画好、裁剪好的内容。
  • MediaSource 用于自定义视频播放器。 MediaSourceMedia Source Extensions 背后的对象,支撑着自适应流媒体和自定义缓冲的视频------你以编程方式构建这个流,仍然需要一个 URL 交给 <video src>useObjectUrl 可以直接接受它,和接受 Blob 一样。

天生 SSR 安全

URL.createObjectURL 在服务端根本不存在------没有 DOM,也就没有 Blob URL 可铸造。useObjectUrl 在服务端渲染期间完全不会碰 URL API;它会一直返回 undefined,直到 effect 在客户端运行为止,所以不需要记得写 typeof window 判断,也不会有服务端崩溃需要排查。

文件处理三件套

useObjectUrl 是一小组 hook 里的一环,这组 hook 覆盖了文件从选取、拖放到预览的完整生命周期:

Hook 角色
useFileDialog 打开原生文件选择器,返回选中的 FileList
useDropZone 把任意元素变成文件的拖放目标
useObjectUrl 把得到的 File/Blob 转成安全、自动回收的预览 URL

useFileDialoguseDropZone 的结果直接接入 useObjectUrl,预览这一步就完全不需要写清理代码。

要点总结

  • 核心问题是一个配对 bug。 每一次 URL.createObjectURL() 都需要一次匹配的 URL.revokeObjectURL(),而在每一个展示文件预览的组件里重新推导这个配对关系,正是内存泄漏悄悄出现的地方。
  • useObjectUrl(object) 接受一个 Blob(或 File)或 MediaSource,返回 URL 字符串,在来源存在之前则是 undefined
  • 回收是自动的,在两个真正重要的时机都会触发:来源变化,以及组件卸载。
  • 覆盖的不只是图片预览:Blob API 响应、canvas.toBlob() 导出,以及用于自定义视频播放器的 MediaSource
  • SSR 安全 ------在服务端返回 undefined,不访问 URL API,不需要写任何守卫代码。
  • 搭配 useFileDialoguseDropZone,选取、拖放、预览三步都不需要手动管理生命周期。

@reactuses/core 里拿走它,别再为漏掉的 revokeObjectURL 调用排查组件了。

相关推荐
kyriewen17 小时前
我用AI写了半年代码——回头看,这5个能力正在退化
前端·javascript·ai编程
大龄秃头程序员19 小时前
iOS 客户端视角扫盲:WKWebView 里 window、messageHandlers 与 Native 回调到底怎么工作?
javascript
GuWenyue19 小时前
90%前端写React+TS都踩坑!从组件类型、单向数据流到本地存储完整实战
前端·react.js
妙码生花19 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(四十五):前端远程下拉输入组件
前端·javascript·vue.js
Listen·Rain21 小时前
AGENTS.md — Vue 3 Frontend Development
前端·javascript·vue.js
BioRunYiXue1 天前
技术干货 | LiP-MS全流程解析:从实验设计到数据分析
大数据·前端·javascript·人工智能·算法·数据挖掘·数据分析
南风知我意啊1 天前
Vue3图片缩放拖拽组件全攻略
前端·javascript·vue.js
布兰妮甜1 天前
暗黑模式一键切换完整方案(CSS 变量 + 本地存储)
javascript·css·web开发·用户体验·前端工程化
早睡早起身体好1231 天前
Vue 3 路由与懒加载入门:从零完成一个迷你学习中心
javascript·vue.js·学习