利益相关声明:作者与文中产品有直接的利益相关(开发者、自家产品等)

一、那句让人恼火的提示

文件太大,请压缩后重试

我猜你见过这句话。可能是在某个在线格式转换工具上,可能是在某个压缩图片的网站上,也可能是在某个视频处理页面上。你手上那台电脑刚剪完一段 4K 素材,同时开着二十个标签页和一个虚拟机,但这个网页坚持认为,它处理不了一个 120MB 的文件。

你大概率不会去追究原因,关掉页面换一个工具就是了。但如果你愿意花几分钟听一个开发者的自述——过去半年里,是我在对无数人说这句话。

我做的是一个在浏览器里从视频提取图片的工具,专业点叫「抽帧」:你丢进去一段视频,它按你指定的间隔或数量把画面导出成图片,全程不上传,所有计算都在你自己的浏览器里完成。它是个很小的工具,但用的人不少,每天要处理相当数量的文件。

这篇文章要讲的是我在迭代产品的过程中犯的一个错误:我盯着一个数字优化了半年,那个数字一直在涨,而它一直在骗我。 更麻烦的是,这个错误骗过我之后,代价是由用户承担的——具体形式就是那句"文件太大,请压缩后重试"。

二、网页为什么总是这么保守

要讲清楚这个错误,得先讲清楚:在浏览器里处理视频,到底难在哪。

一段视频文件,本质上是一个容器(MP4、MOV、MKV、AVI 这些后缀就是容器格式)里装着用某种编码方式压缩过的画面数据。要把画面提取出来,就必须先把它解码还原。这件事在你的电脑上很轻松——播放器天天在干——但在网页里,情况完全不同。

浏览器给了两条路。

第一条路是借用硬件加速。 现代浏览器提供了一套叫 WebCodecs 的接口,它能直接调用你设备上的专用解码芯片——Mac 上是 VideoToolbox,Windows 上可能是 Intel Quick Sync 或者显卡的解码单元,手机上是 MediaCodec。这条路快得惊人,在一台新款 MacBook 的 Chrome 上,解码 1080p 视频可以跑到每秒数百帧甚至接近一千帧。

代价是它挑食。硬件解码器只认几种主流编码格式,遇到不认识的东西就直接罢工。而且不同浏览器的支持程度差别巨大——同样一台机器,Chrome 上跑得飞快,Safari 上可能只有前者的四分之一速度,Firefox 上又是另一个数字。

第二条路是纯软件解码。 把大名鼎鼎的 FFmpeg 编译成 WebAssembly 塞进浏览器里,用 CPU 硬算。这条路的好处是什么格式都能吃,MKV、AVI、各种冷门编码统统不在话下。

代价是慢,而且吃内存。官方给出的基准测试里,同样的转码任务,原生 FFmpeg 用 5.2 秒,编译成 WebAssembly 的单线程版本要 128.8 秒,多线程版本也要 60.4 秒——慢了 12 到 25 倍。内存问题更致命:浏览器给单个标签页的内存是有限的,Chrome 在接近 2GB 时就可能直接杀掉整个页面。处理一个大文件时,解码出来的原始画面数据会瞬间膨胀到原文件的几十倍,一不小心页面就白了。

所以我的策略是分流:MP4 这类主流格式走第一条路,其他格式走第二条路,中间再加几层兜底方案——硬件解码失败就退回软件解码,软件解码也不行就退回最原始的"播放器 + 截图"模式。

然后是限制。 因为软件解码这条路随时可能撑爆内存,我必须设一个门槛:超过多大的文件不让处理。问题是,网页不知道用户的电脑有多强。我唯一能确定的是,如果按最强的设备设限,弱设备上的用户会遇到页面崩溃;而如果按最弱的设备设限,至少没人会崩。

于是我做了工程上最省事的选择:一刀切。 电脑端一个数,手机端一个数,所有人一视同仁。

这就像一条路,因为可能有自行车经过,所以给所有车都限速 15 公里。跑车很委屈,但至少不会出事。

而且我有数据支持这个决定。上线之后,我持续做兼容性优化——补 fallback、修解码边界情况、调整内存释放时机——处理成功率从 63% 一路提升到了 87%。这是个相当扎实的提升,每一个百分点背后都是具体的 bug 修复。半年时间,我都在为这条曲线感到满意。

三、那个 87% 是假的

真正让我意识到问题的,是一个很朴素的疑问:我的成功率里,到底统计了哪些人?

答案是:只统计了被我允许处理的那些。

一个用户传了个 200MB 的文件,超过限制,我弹窗拒绝了他。他关掉页面走了。在我的统计里,这件事从未发生过——他既不算成功,也不算失败,他压根没被记录下来。

想明白这一点之后,一个荒唐的循环就浮现出来了:

我把门槛卡得越严,能进来的文件就越简单,我的成功率就越好看。

顺着这个逻辑推到极端:我可以把限制降到 10MB。那样只有最小最简单的视频能进来,成功率大概能到 99%。那时候我一定会觉得自己做得非常棒——而实际上,绝大多数用户连门都进不去。

这个道理换个说法更明显:如果一场考试的及格率只统计通过考试的人,那及格率永远是 100%。

听起来蠢到不需要人提醒。但我用了半年才反应过来,因为那个数字确实在涨,而且它涨的原因也确实是我修好了很多 bug。一个正在上涨的、并且上涨得有理有据的数字,是最容易让人停止思考的东西。

于是我重新数了一遍。把被拒绝的人也算进来,同一件事会变成两个数字:

 

 

 我看到的用户经历的
100 个人想提取图片100 人
被弹窗挡在门外不计入统计30 人失望离开
进入处理流程70 人70 人
最终拿到图片61 人61 人

最终数字

87%

61%

 

同一件事,两个数字。差的那二十几个百分点,是我看不见的用户。

更让人难受的是第二层结论:我优化了半年的那 24 个百分点,只作用在"已经进了门的人"身上。 那些被挡在门外的,无论我把引擎优化得多好,他们的体验都是一模一样的——一个弹窗,一句"文件太大"。

四、设备之间的差距,比你以为的大得多

发现问题之后,下一个问题是:那些被挡在门外的人,本来能不能成功?

这就得回到最开始那个场景:一台 16GB 内存的高配笔记本,和一台三年前的入门机型,被允许处理的文件大小是一样的。

我一直隐约觉得这不太对,但没意识到差距有多大。真去查了各种基准测试数据之后,我有点意外——这不是快一点慢一点的差距,是数量级的差距。

几个具体的数字:

硬件解码这条路上,同样一段 1080p 视频,在 MacBook Pro 的 Chrome 上解码可以达到每秒近千帧;同样的任务放到 Safari 上大概只有四分之一;而在手机上,每解码一帧要花十几毫秒,编码一帧要花七十毫秒左右——比桌面端慢了几十倍。

软件解码这条路上,差距更直接地体现在内存上。一台 2GB 内存的入门手机,浏览器能分配的内存本来就紧张,处理稍大的文件就会被系统杀掉;而一台 32GB 内存的桌面机,同样的任务连风扇都不会响。CPU 核心数的差距同样明显:软件解码可以多线程并行,2 核和 16 核之间是好几倍的差别。

把这些差距叠加起来,高端设备和低端设备在浏览器里处理视频的能力,差距可以达到十倍甚至上百倍。

而我给它们设了同一个限制。

那条限速 15 公里的路,问题不只是跑车被拖累了。更麻烦的是,这个限速对某些老旧的自行车来说,可能还是太快了。

五、让网页学会看人下菜碟

好消息是,浏览器其实是可以"问"设备的。

有几个接口能让网页大致了解当前设备的情况:navigator.deviceMemory 会返回一个粗粒度的内存值(出于隐私考虑,它只会给出 2、4、8、16 这样的档位,而不是精确数字);navigator.hardwareConcurrency 返回 CPU 的逻辑核心数;VideoDecoder.isConfigSupported() 可以直接问"这台设备能不能硬件解码这种编码格式"。还有一个更精细的 Media Capabilities 接口,能回答"解码这个分辨率和码率的视频,是否流畅、是否省电"。

这些信号都不精确,但它们足够把设备大致分出档次。

接下来是个工程上的选择题。如果给每一种设备组合都写一套规则,会写出多少种情况? 内存 4 档、核心数 4 档、支不支持硬件加速 2 种、电脑还是手机 2 种——光是这四个维度就有六十多种组合,再加上格式和浏览器,这个表格永远写不完,也永远维护不了。

所以我换了个思路:不给设备分类,给设备打分。

把各项能力按权重加权成一个分数——内存权重最高,因为它是最常见的失败原因;核心数其次,它影响速度;有没有硬件加速再次,它决定走哪条路。得到一个分数之后,限制就是这个分数乘以一个系数,再加上一个上限和一个下限做保护。

这样一来,六十多种组合被压缩成了一条连续的曲线:设备越强,限制自动越宽;设备越弱,限制自动越紧。 代码里没有一个条件判断,只有一个公式。

调整后的效果大致是这样:

 

 

你的设备之前允许现在允许
高配 MacBook / 游戏本100MB约 300MB
普通笔记本100MB约 175MB
中端手机50MB约 75MB
入门手机50MB约 40MB

(具体数值仍在随数据持续校准中。)

你可能注意到了最后一行——入门手机的限制反而收紧了。

这不是笔误。数据告诉我,那些设备本来就撑不住 50MB 的文件。让它们试的结果是:用户等两分钟,进度条走到一半,然后看到一个失败提示,或者页面直接白掉。与其让它失败,不如早点告诉它做不到。 至少用户能省下那两分钟,去换一个更合适的方案。

所以这次改动的准确描述不是"放宽限制",而是**"该松的松,该紧的紧"**。

这里还有一个我一开始没想到的收获:这两件事优化的其实是同一个数字。 给高端设备放宽,是让更多人能进门;给低端设备收紧,是让进了门的人少失败。以前我只盯着后者,因为前者根本不在我的统计里。

六、这套东西怎么持续变准

必须承认,上面那些数字不是算出来的真理,它们是估计值

浏览器给的设备信息很粗糙,内存只有几个档位,而且它反映的是设备总内存,不是浏览器实际能用的内存。同样标称 8GB 内存的设备,用户开着三十个标签页和不开标签页,实际可用空间天差地别。

所以真正的做法不是一次性算出正确答案,而是建立一个能自我修正的循环

每一次处理,无论成功还是失败,都记录下当时的设备情况、文件大小、走的哪条路径、耗时多久、失败的话是什么原因。跑一段时间之后,这些数据会自己画出边界——比如某个内存档位的设备,处理超过某个大小的文件时成功率明显跌下来,那个拐点就是这类设备的真实上限。

然后用这个真实上限去反推公式里的系数,再灰度上线一小部分流量验证,达标了扩大范围,不达标就回滚。每隔几周做一次,模型就精确一点。

这个循环里没有任何 AI,全程就是埋点、统计、算数、改配置。但它有个好处:它会随时间自己变强。 用的人越多,数据越丰富,对设备的判断就越准;判断越准,能安全放开的空间就越大。

而且这套"设备到底能干多少活"的知识,不只对抽帧有用。任何要在浏览器里跑重计算的功能——图片批处理、格式转换、本地模型推理——面对的都是同一个问题:这台设备到底扛得住多少。 今天为抽帧积累的东西,明天可以直接复用。

七、我真正学到的

技术层面的结论是"一刀切是错的",但这个结论其实不值钱,大部分人凭直觉就知道。

我真正学到的是另一件事:每当看到一个漂亮的数字,先问一句——这个数字是怎么数出来的?谁没被数进去?

我的 87% 没有说谎,它诚实地反映了"进入处理流程的任务里有多少成功了"。它的问题在于,"进入处理流程"这个前提是我自己设的,而我可以通过收紧这个前提,让数字变得任意好看。

一个能被自己的策略操纵的指标,是不能用来指导决策的。

更隐蔽的地方在于,这类错误不会报错。一刀切的限制没有 bug,没有崩溃,没有报警,它只会安静地把一部分用户挡在门外,而这些用户永远不会出现在你的任何一张报表里。它不会失败,它只会沉默地流失。

如果你也在做一个有门槛的东西——不管是一个工具、一个功能,还是一个流程——这个问题可能同样值得问一遍:你的成功率里,有没有一群人是被你自己挡在统计之外的?

感悟

新的逻辑已经上线了。如果你手上有台性能不错的机器,可以去试试: 视频转图片工具 
优先试以前会被拒绝的大文件——不需要注册,不需要登录,视频从头到尾都留在你自己的浏览器里,我这边看不到你的文件,也没有服务器去存它。

如果它在你的设备上还是失败了,欢迎告诉我具体是什么设备、什么文件。对我来说,一次失败比一次成功有用得多——前者能让模型变准,后者只是让数字好看。

披露:本文提到的工具由我本人开发和运营。文中的成功率数据来自产品自身的埋点统计,设备性能对比数据来自公开的基准测试资料。