去年有组数据挺有意思:Vercel上部署的Node服务,37%的构建故障和路径解析有关。这个数字来自他们内部工程师的分享会,没写进官方文档。
Remotion团队上周栽的跟头,恰好落在这37%里。而且一栽就是两次——两个bug连环触发,生产环境的视频渲染直接静默失败。没报错,没日志,就是输出空视频。
第一次:相对路径的"薛定谔式"生效
他们的部署流程是这样的:构建时把Remotion项目打包成bundle,上传到Vercel Sandbox做快照,渲染请求来时直接恢复快照,跳过重复打包。冷启动从秒级降到毫秒级。
问题出在addBundleToSandbox这个函数。它接收一个bundleDir参数,团队传的是相对路径".remotion"。
本地跑没问题,CI跑没问题,但Vercel的构建环境里,脚本执行时的CWD(当前工作目录)会漂移。不是每次都漂移,是"某些构建环境"才漂移——这最要命,你复现不了。
路径解析到错误目录,上传"成功"了零个文件,快照是空的。渲染时恢复空快照,输出自然也是空的。
修复就一行代码,用__dirname锚定:
从脚本自身位置算绝对路径,而不是相信执行时的CWD。这是Node.js脚本写了十几年还在踩的坑。
Remotion的维护者Jonny Burger在PR里写:「Never trust relative paths in build scripts. Scripts can be invoked from anywhere.」
第二次:public文件夹的嵌套陷阱
路径修好后,构建又挂了。这次有报错:addBundleToSandbox遇到public/fonts/Inter.woff2这种嵌套路径时直接抛错。
原因是Remotion的打包器默认会把项目public/目录复制到输出。但Vercel Sandbox的API内部用mkDir创建目录,不是递归的——它不会自动创建父目录。先遇到public/fonts/xxx时,public/还不存在,直接崩溃。
团队给了两个解法。方案A最干净:打包时传publicDir: null,跳过复制静态资源。前提是你的视频合成不依赖public/里的字体、图片。
方案B是妥协:让打包器复制完,上传前手动删掉public/目录。用Node的fs/promises里的rm递归删除。
他们选了方案A。因为Remotion的渲染逻辑本来就把静态资源内联进bundle了,public/是冗余的。
两个bug的共通点
都是"静默失败"模式。第一个上传成功但内容为空,第二个在特定文件结构才触发。这种bug比直接崩溃难查十倍——你得先怀疑数据,再怀疑路径,再怀疑平台行为差异。
Jonny Burger在复盘里提到,Vercel的构建环境文档对CWD行为描述模糊。「我们以为CI和本地一致,但Vercel的构建容器在某些阶段会切换工作目录。」
这其实是serverless平台的通病。AWS Lambda、Cloudflare Workers都有类似陷阱:本地模拟器和真实运行时的文件系统行为存在细微差异。你本地测一百遍没问题,上线就抽风。
Remotion团队现在加了两个防御:所有路径强制绝对化,构建流程里加一步验证snapshot内容非空。不是相信平台,是假设平台会背叛你。
有个细节挺有意思:他们用的import.meta.url转__dirname这套代码,在ESM和CommonJS混用的项目里经常写错。Node.js 20+其实有import.meta.dirname原生支持了,但Remotion要兼容旧版本,还得手动拼。
最后留个尾巴。他们修复后监控了一周,发现有个边缘case还在漏:如果项目路径里带空格,fileURLToPath返回的URL编码不会自动解码,需要再包一层decodeURIComponent。这个补丁还没合进主分支。
你的构建脚本里,还有多少个相对路径在"薛定谔式"生效?
热门跟贴