「在浏览器里上传一个几 G 的视频」是这个后台最硬的前端问题,而且它有一批只在浏览器环境下才出现的约束。
第一版用了一个 input file 加一次性 POST,问题全面爆发。
一是大文件一次性上传几乎必然失败。4GB 的文件传半小时,中间任何一次网络抖动、代理超时、浏览器标签被系统回收,整个上传从零开始。创作者传三次都失败之后就不传了。
二是计算文件哈希把页面卡死。为了做秒传要算文件哈希,在主线程里读 4GB 文件算 MD5,页面卡住几十秒完全无响应,创作者以为浏览器崩了。
三是刷新页面进度全丢。进度只存在内存里,创作者手滑刷新或者关了标签,重开要从头传。
四是发布后才发现不合规。封面尺寸不对、时长超限、码率过高,上传完等了半小时转码,最后审核驳回。创作者的反馈是「你们不能早点告诉我吗」。
所以四个设计:分片上传加断点续传、哈希计算放 Web Worker、进度持久化到 IndexedDB、发布前预检并给出可执行建议。
video 元素读时长和分辨率是毫秒级的,如果时长超限就没必要花几十秒算哈希。顺序按「成本从低到高」排。采样哈希牺牲了严格性换速度,这个取舍要说清适用边界。它足以判断「是不是同一个文件」(用于秒传和续传),但不能用于版权判定或严格去重——那些需要全量哈希甚至内容指纹。我们的做法是采样哈希用于前端秒传判定,服务端在合并分片后再做一次完整校验,两层配合。
分片直传对象存储,而不是经过我们的服务器。直传的好处是不占用应用服务器带宽、上传速度更快(对象存储的边缘节点更近)。代价是要签发上传凭证并严格限制(路径前缀、大小上限、有效期),以及不能信任客户端说的「传完了」——服务端必须去对象存储核实分片存在且大小正确。
踩过的坑:Worker 里一次性读整个文件导致内存爆掉。第一版在 Worker 里用 FileReader.readAsArrayBuffer 读整个 4GB 文件,浏览器直接崩溃(标签页崩溃提示)。修法是用 File.slice 分块读,读一块算一块,读完就释放。教训是:处理大文件时任何「一次性读进内存」的操作都是隐患,无论在主线程还是 Worker 里——Worker 只解决了「不卡主线程」,没解决内存。
踩过的坑二:本地记录说已传 80 片,服务端只有 60 片,续传后合并失败。某些分片上传请求实际失败了但客户端记成功(响应解析问题),本地记录比服务端多,续传时跳过了那些分片,最后合并时服务端发现缺片,整个上传失败,而创作者已经等了半小时。修法是续传前必须以服务端返回的已传分片列表为准,本地记录只用于快速恢复界面显示。教训是:客户端记录的状态永远不能作为「事实」,必须以服务端为准。
没做的部分:没做上传的后台化(关闭标签页后继续上传)。浏览器里做不到真正的后台上传(Service Worker 也无法在标签关闭后持续大文件上传)。只能做到「刷新页面可续传」,并明确告知创作者不要关闭标签页——这个能力边界要如实说明,不能承诺做不到的事。
4GB 在 2MB/s 下可完整上传:用浏览器开发者工具限速到 2MB/s,传一个 4GB 的真实视频文件,验证能完整走完(约半小时)。过程中要制造干扰:断网几十秒再恢复、刷新页面、切到其他标签,验证每种情况下都能继续。这类能力靠演练而不是数字说明,要能说出演练了哪些干扰场景。
哈希计算期间页面可交互:用 Performance 面板录制哈希计算全程,检查主线程有没有长任务(超过 50ms 的任务块)。放到 Worker 之后主线程应该基本空闲。要报出哈希计算的耗时(采样哈希对 4GB 文件大约几秒),以及对比全量哈希的耗时,说明采样的收益。
秒传命中:同一文件重复上传,验证第二次直接完成。可以报秒传的命中次数——如果有埋点,这个数字证明功能被真实使用(创作者多平台分发时经常传同一个文件)。
并发分片数的选择依据:压测不同并发数(1、2、4、8)下的整体上传耗时,报出「为什么选 3 到 4」——通常会发现超过某个数之后耗时不再下降甚至上升(抢带宽、超时增多)。能给出这条曲线比直接说「我用了 4 个并发」有说服力。
不要报「上传成功率 99%」。上传失败的主要原因是创作者的网络,我们改不了。可以报的是「因客户端逻辑导致的上传失败」(比如那个「本地记录与服务端不一致导致合并失败」的 bug 修复前后的对比),这个是我们能控制的。
FileReader.readAsArrayBuffer 读整个 4GB 文件,浏览器直接崩溃(标签页崩溃提示)——因为 4GB 数据要一次性进内存。修法是用 File.slice 分块读,读一块算一块,读完立刻释放引用,这样内存占用只有一个分块的大小。教训是处理大文件时任何「一次性读进内存」的操作都是隐患,无论在哪个线程。另外还有个优化:采样哈希——不需要读完整个文件,只读首尾各若干分片加中间等距抽样,再拼上文件大小作为指纹,速度快一个数量级。验证方式是用 Performance 面板录制全程,检查主线程有没有超过 50ms 的长任务,放到 Worker 后主线程应该基本空闲。
创作者数据看板和内部运营报表最大的差异是:它的用户会拿数字来质问你。创作者的收入和这些数字直接相关,他会逐个核对、会截图对比、会在社群里和其他创作者比。
第一版是标准的图表页,问题不在技术而在「说不清」。
一是同一个指标在不同页面数字不一样,创作者认为我们造假。首页的「总播放量」和视频详情页的播放量加起来不相等。原因其实很正常——一个是去重后的、一个是不去重的,一个含站外播放、一个不含,但页面上完全没说明。创作者的结论是「你们的数据不准」。
二是数据延迟没有标注,创作者以为是实时的。他刚发的视频看播放量是 0,以为没人看,实际是统计还没跑到。他会在这个错误认知下做决策(比如立刻删了重发)。
三是长时间跨度的图表卡死。90 天 × 多个指标 × 分小时粒度,几万个数据点全丢给图表库,渲染卡住好几秒,缩放拖动完全不流畅。
四是切换维度时图表和筛选条件对不上。快速切了几次维度,慢的那个请求最后返回覆盖了界面,图表显示的是上一个维度的数据,而筛选器显示的是当前维度。创作者截图来问「这是什么数据」。
所以四个设计:每个指标都有可点开的口径说明、显式标注数据延迟与截止时间、长跨度曲线降采样、请求竞态用序号控制。前两条是这个模块最重要的部分——它们不是技术问题,但决定了创作者是否信任这个产品。
把「口径说明」当成核心功能而不是文档补充,这是这个模块最重要的判断。技术上它只是几个弹窗,但它解决的是整个产品的信任问题。创作者不信任数据的后果很严重——他会认为平台限流、会去社群里传播、会流失。我在这个项目里的体会是:面向外部用户的数据产品,「可解释性」的优先级高于「功能丰富度」。
显式标注延迟看起来是「暴露缺点」,实际是减少投诉。产品最初担心「写了延迟创作者会觉得我们的数据不及时」。但实际情况是不写延迟的投诉远多于写了延迟的抱怨——因为前者是「你们数据不准」(信任问题),后者只是「希望更快」(需求问题)。这个判断在很多产品决策里通用:如实告知局限比隐藏局限的总成本低。
降采样在服务端做,代价是灵活性下降。服务端决定粒度,前端想要不同粒度就要重新请求。但把降采样放前端意味着要传几万个点,网络和解析开销都大。折中是服务端按跨度给合适粒度,前端支持「按视图范围重新请求」,这样既控制了传输量又保留了看细节的能力。
踩过的坑:降采样用等距抽样,创作者的爆发日在曲线上消失了。90 天曲线按等距抽样降到 200 个点,某个单日播放量暴涨的尖峰正好落在被抽掉的位置,曲线上完全看不到。创作者说「我那天明明爆了,你们图上没有」。修法是每个采样区间内保留最大值和最小值,保证极值不丢失。教训是:降采样的目标是「保持曲线的视觉形状」而不是「均匀取点」,极值点是形状的关键。
踩过的坑二:维度切换的竞态导致图表与筛选器错配。创作者快速切了几次流量来源维度,最慢的那个请求最后返回覆盖了界面,图表显示的是「搜索来源」的数据,而筛选器显示的是「推荐来源」。他截图来问「这是什么数据」,我们查了很久才发现是竞态。修法是请求序号比对丢弃过期响应。这和旅游客户端切换日期的坑是同一类问题——任何「用户输入驱动的异步查询」都必须处理竞态,搜索联想、筛选、分页、维度切换都是。
没做的部分:没做数据的自定义看板(让创作者自己拖拽组合想看的指标)。MCN 机构提过,但需要一套配置化的图表编排,工作量不小,而且大部分个人创作者用不上,当时排在后面。
90 天多指标曲线渲染耗时:量「数据到手」到「图表绘制完成」,降采样后约 200ms。必须同时说明降采样后的实际点数——「90 天多指标」听起来是几万个点,实际渲染的只有几百个,这才是 200ms 的原因。报「原始点数」和「渲染点数」两个数字才能说明设计。
降采样保留极值的验证:造一组含明显尖峰的数据,降采样后检查尖峰是否仍在曲线上、位置是否正确。这是个可精确验证的点,也正是我们踩过坑的地方。
竞态控制的验证:用工具让第一个请求延迟数秒返回,然后快速连续切换三次维度,验证最终图表对应的是最后一次选择、且筛选器与图表一致。改造前每次都能复现错配。
「口径咨询被消化」怎么量化:报「为什么两个页面数字不一样」这类咨询工单的数量变化,以及口径说明的点开次数。两个一起看才有意义——如果说明没人点但咨询也降了,可能是别的原因。点开次数还能反映哪些指标最容易引起疑惑,指导后续优化。
不要报「数据准确率」。数据准不准是数仓和埋点的事,前端只负责如实呈现。也不要报「创作者满意度提升」——没有可靠测量方式。可以报的是可核查的事实:覆盖了多少个指标的口径说明、延迟标注覆盖了哪些指标、竞态错配的修复验证。
评论管理在创作者后台里是使用频率最高的功能之一,而且它的数据量级是前面几个模块里最大的——一条爆款视频几万条评论,其中相当比例是需要处理的(广告、引流、攻击性言论)。
第一版是标准的分页列表加单条操作,几乎不可用。
一是逐条处理完全跟不上。几万条评论里有几百条广告,创作者要一条条点删除,每次点完等刷新。他的实际做法是干脆关闭评论区——这对平台是损失(评论是重要的互动数据)。
二是分页处理时页码会错乱。删掉几条之后,后面的评论往前移,翻到第二页会跳过一些评论(因为用的是 offset 分页)。创作者以为处理完了,实际漏了一批。
三是同样的垃圾评论反复出现。某个引流广告的固定话术,创作者今天删了明天又来。他需要的是「以后这类评论自动屏蔽」而不是每天手动删。
四是误删无法恢复。批量选中时手滑多选了正常评论,删了就没了。粉丝的评论被误删会引发不满,而创作者根本不知道自己删了什么。
所以四个设计:虚拟列表加游标分页支撑数万条、跨页批量选择与条件筛选、关键词自动屏蔽规则、删除操作给撤销窗口。
「规则命中进待确认而不是直接删除」是有代价的——创作者要多一步操作。他会问「为什么不能自动删掉」。理由是规则的准确性完全由他自己保证,而他配的规则大概率过宽:把一个常用词加进关键词,可能屏蔽掉几百条正常评论而他毫不知情,等发现时粉丝已经在抱怨「我的评论怎么没了」。待确认列表让误伤是可见可恢复的。折中做法是:对明确的高危模式(含手机号、微信号、外链)允许直接删除,普通关键词进待确认——按规则的确定性分级。
「全选筛选结果」传条件而不是传 ID,代价是前后端的语义要严格一致。创作者点全选时看到的是「1247 条」,服务端按条件处理时可能因为期间有新评论进来而变成 1250 条。做法是全选时把「筛选条件 + 时间上界」一起传,服务端只处理这个时间点之前的评论,保证数量与创作者看到的一致。这个细节不处理会出现「我选了 1247 条,结果处理了 1250 条」的困惑。
踩过的坑:批量删除用 offset 分页导致漏处理。创作者删了第一页的 50 条,翻到第二页时后面的评论已经前移,第二页实际显示的是原来第 100 到 150 条,中间第 50 到 100 条被跳过了。他以为处理完了,实际漏了一批广告,粉丝还在举报。修法是游标分页。教训是:任何「边遍历边删除」的场景都不能用 offset 分页——这和之前评论后端、Feed 分页的判断一致。
踩过的坑二:创作者配了一个过宽的关键词,静默屏蔽了大量正常评论。他把一个常见词加进屏蔽规则(本意是屏蔽某类广告),结果几百条正常评论被自动屏蔽,粉丝在其他地方抱怨「评论被删」,创作者自己完全不知道,还以为是平台在删。修法就是「规则命中进待确认」,并且给每条规则展示命中数量——命中数异常高的规则会很显眼,创作者自己就会去检查。教训是:把规则配置权交给非技术用户时,必须让规则的效果可见,否则错误会静默累积。
没做的部分:没做评论的情感分析与自动分类(自动识别攻击性言论、正向互动)。这需要模型支持,而且误判的代价(把正常评论判成攻击性)不好承担,当时只用平台已有的「疑似广告」标记。
数万条评论列表的滚动流畅度:造一条有 5 万条评论的测试视频,用 Performance 面板录制持续滚动过程,检查有无长任务和掉帧。要报出虚拟列表实际渲染的节点数(可视区加缓冲大约几十个)——这才是流畅的原因。同时要报测试机型。
批量处理的规模与耗时:报「一次批量可处理多少条」以及超过阈值转异步任务的阈值是多少、怎么定的(压测出的单请求超时边界)。异步任务的完成时长也要报,否则会被问「你只是把慢转移到了后台」。
「漏处理」的验证:构造场景——列表有 200 条评论,删除前 50 条后翻页,验证第二页显示的是第 51 到 100 条而不是第 100 到 150 条。这是个可精确验证的点,也正是我们踩过的坑。
规则的效果与误伤:报「每条规则的命中数量」以及「待确认列表中被创作者恢复的比例」。后者直接反映规则误伤情况——如果某条规则命中的评论大量被恢复,说明规则配得太宽。这个数据比「规则拦下多少广告」更有价值,因为它暴露问题而不只是展示成绩。
不要报「垃圾评论清理率」。需要「垃圾评论总量」作为分母,而那个数字不可知。也不要报「评论管理效率提升 N 倍」——没有可比基线。可以报的是可核查的事实:单次批量的处理量、筛选条件覆盖了哪些场景、撤销窗口拦下过多少次误删。
没有匹配的内容,换个关键词试试。
项目拆解 · 创作者后台(Vue)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据