移动端 360° 商品展示方案:从 20MB 雪碧图到 800KB 丝滑视频
一、问题的起点
做电商商品详情页的 3D 展示,摆在面前的通常有三条路。但前两条都走不通。
路线 1:多帧图片序列拼接(Sprite 拼图)
在影棚用 36 或 72 个角度拍摄商品照片,前端通过鼠标/触摸事件动态切换 img 的 src 或控制一张超大 Sprite 图的 background-position。
致命问题 :72 张高分辨率图片合起来起码 20MB。不预加载,用户划动时严重闪烁白屏;预加载,用户带宽被瞬间吃满,首屏直接卡死。直接 Pass。
路线 2:Three.js / WebGL 原生模型渲染
直接把 glTF、USDZ 模型加载到页面,用 WebGL 实时渲染。
致命问题:移动端 GPU 加速会导致手机迅速发烫。更糟的是,为了网页端流畅,美术必须对模型极度减面和压缩贴图,渲染出来的材质有一股廉价的塑料感,完全失去高端商品的质感。
路线 3:3D 离线渲染视频 + 视频帧控制(Video Scrubbing)
让 3D 美术在 Blender 或 C4D 里用顶级离线光线追踪渲染器,渲染一段商品匀速自转一圈的高保真视频(比如 2 秒、60 帧)。
前端用 video 标签嵌入,禁用自动播放。用户左右滑动时,不播放视频,而是计算滑动位移量,直接修改 video.currentTime,实现手动拨动商品旋转。
这套方案的优势很明显:
- 渲染质量是电影级的,金属光泽、皮革纹理完美保留
- 视频压缩率极高,一段 1080P 自转视频可压缩到 1MB 左右
- 加载极快,完全不消耗手机 GPU
但有一个致命问题:移动端快速滑动时,视频画面会产生严重卡顿、抖动和明显的延迟响应。
二、根因分析:为什么移动端视频拖动会卡顿?
要解决这个问题,必须先理解视频编码的底层机制。
视频之所以能被压缩得这么小,是因为它采用了帧间压缩(Temporal Compression) 技术。视频帧被分为三种类型:
| 帧类型 | 全称 | 说明 |
|---|---|---|
| I 帧 | Intra-coded picture(关键帧) | 包含完整的单张静态图像,可独立解码(相当于一张完整的 JPEG) |
| P 帧 | Predicted picture(预测帧) | 只记录与前一个帧的差异数据,解码它必须先解码前面的帧 |
| B 帧 | Bi-directional predicted picture(双向预测帧) | 记录与前后帧的差异,解码它需要同时参考前一帧和后一帧 |
为了最大化压缩体积,常规视频的 I 帧间隔(GOP 尺寸)非常大,通常 250 帧以上。这意味着每隔 250 帧才有一个完整的关键帧。
当你用手指快速拖拽商品时,浏览器需要将 video.currentTime 频繁设置到任意时间点(比如从第 10 帧跳到第 45 帧)。
此时,浏览器的视频解码器会瞬间崩溃------为了渲染第 45 帧(P/B 帧),它必须强行回溯到第 1 帧(最近的 I 帧),然后一口气在内存中把中间的 44 帧全部计算一遍。
如果用户在屏幕上快速来回摩擦,解码器的计算量会呈指数级飙升,导致严重的掉帧、卡死和发热。
#mermaid-svg-N9RAOqDp8xeBghJF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-N9RAOqDp8xeBghJF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-N9RAOqDp8xeBghJF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-N9RAOqDp8xeBghJF .error-icon{fill:#552222;}#mermaid-svg-N9RAOqDp8xeBghJF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-N9RAOqDp8xeBghJF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-N9RAOqDp8xeBghJF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-N9RAOqDp8xeBghJF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-N9RAOqDp8xeBghJF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-N9RAOqDp8xeBghJF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-N9RAOqDp8xeBghJF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-N9RAOqDp8xeBghJF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-N9RAOqDp8xeBghJF .marker.cross{stroke:#333333;}#mermaid-svg-N9RAOqDp8xeBghJF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-N9RAOqDp8xeBghJF p{margin:0;}#mermaid-svg-N9RAOqDp8xeBghJF .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-N9RAOqDp8xeBghJF .cluster-label text{fill:#333;}#mermaid-svg-N9RAOqDp8xeBghJF .cluster-label span{color:#333;}#mermaid-svg-N9RAOqDp8xeBghJF .cluster-label span p{background-color:transparent;}#mermaid-svg-N9RAOqDp8xeBghJF .label text,#mermaid-svg-N9RAOqDp8xeBghJF span{fill:#333;color:#333;}#mermaid-svg-N9RAOqDp8xeBghJF .node rect,#mermaid-svg-N9RAOqDp8xeBghJF .node circle,#mermaid-svg-N9RAOqDp8xeBghJF .node ellipse,#mermaid-svg-N9RAOqDp8xeBghJF .node polygon,#mermaid-svg-N9RAOqDp8xeBghJF .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-N9RAOqDp8xeBghJF .rough-node .label text,#mermaid-svg-N9RAOqDp8xeBghJF .node .label text,#mermaid-svg-N9RAOqDp8xeBghJF .image-shape .label,#mermaid-svg-N9RAOqDp8xeBghJF .icon-shape .label{text-anchor:middle;}#mermaid-svg-N9RAOqDp8xeBghJF .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-N9RAOqDp8xeBghJF .rough-node .label,#mermaid-svg-N9RAOqDp8xeBghJF .node .label,#mermaid-svg-N9RAOqDp8xeBghJF .image-shape .label,#mermaid-svg-N9RAOqDp8xeBghJF .icon-shape .label{text-align:center;}#mermaid-svg-N9RAOqDp8xeBghJF .node.clickable{cursor:pointer;}#mermaid-svg-N9RAOqDp8xeBghJF .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-N9RAOqDp8xeBghJF .arrowheadPath{fill:#333333;}#mermaid-svg-N9RAOqDp8xeBghJF .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-N9RAOqDp8xeBghJF .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-N9RAOqDp8xeBghJF .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N9RAOqDp8xeBghJF .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-N9RAOqDp8xeBghJF .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N9RAOqDp8xeBghJF .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-N9RAOqDp8xeBghJF .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-N9RAOqDp8xeBghJF .cluster text{fill:#333;}#mermaid-svg-N9RAOqDp8xeBghJF .cluster span{color:#333;}#mermaid-svg-N9RAOqDp8xeBghJF div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-N9RAOqDp8xeBghJF .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-N9RAOqDp8xeBghJF rect.text{fill:none;stroke-width:0;}#mermaid-svg-N9RAOqDp8xeBghJF .icon-shape,#mermaid-svg-N9RAOqDp8xeBghJF .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N9RAOqDp8xeBghJF .icon-shape p,#mermaid-svg-N9RAOqDp8xeBghJF .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-N9RAOqDp8xeBghJF .icon-shape .label rect,#mermaid-svg-N9RAOqDp8xeBghJF .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N9RAOqDp8xeBghJF .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-N9RAOqDp8xeBghJF .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-N9RAOqDp8xeBghJF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
用户快速滑动
video.currentTime 频繁跳转
目标帧是 I 帧?
直接解码 ✓
回溯到最近 I 帧
解码 I 帧到目标帧之间的所有帧
计算量指数级飙升
❌ 掉帧 / 卡死 / 发热
三、解法:用 ffmpeg 降维打击
既然找到了病因,解法就很明确:通过特定的编码参数,彻底摧毁视频的帧间依赖,让浏览器能以极低的 CPU 开销进行任意帧的瞬间定位(Scrubbing)。
经过对 iOS Safari 和各大安卓浏览器内核的反复压测,最终整理出这行专为移动端交互而生的 ffmpeg 命令:
bash
ffmpeg -i input.mp4 -an -vf "scale=1280:880,fps=30,format=yuv420p" \
-c:v libx264 -profile:v main -level:v 4.0 -preset slow -crf 22 \
-g 6 -keyint_min 6 -sc_threshold 0 -bf 0 -movflags +faststart \
product-360-mobile-gop6.mp4
参数逐个拆解
| 参数 | 作用 | 为什么关键 |
|---|---|---|
-an |
剥离音频 | 商品交互图不需要声音,省带宽,且防止浏览器因媒体策略拦截视频 |
-g 6 -keyint_min 6 |
强行将最大和最小 GOP 长度锁死在 6 帧 | 30fps 下每隔 0.2 秒就有一个完整 I 帧,任意定位最多只需解码 5 个中间帧 |
-sc_threshold 0 |
禁用场景切换检测 | 防止 ffmpeg 擅自插入非等距 I 帧,确保关键帧分布极其均匀,滑动手感绝对线性 |
-bf 0 |
致命一击:彻底禁用 B 帧 | B 帧需要前后双向查找,是来回摩擦交互时卡顿的第一元凶。禁用后只保留 I/P 帧,解码器单向线性解码,速度质变 |
-movflags +faststart |
将索引信息(moov atom)移到文件头部 | 浏览器只需下载开头几个字节就能开始响应拖拽定位,无需等整个视频加载完 |
通过这行命令导出的 1280x880 视频,文件体积控制在 800KB 左右。
#mermaid-svg-cmqPD7HDynQwHF9m{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-cmqPD7HDynQwHF9m .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-cmqPD7HDynQwHF9m .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-cmqPD7HDynQwHF9m .error-icon{fill:#552222;}#mermaid-svg-cmqPD7HDynQwHF9m .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-cmqPD7HDynQwHF9m .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-cmqPD7HDynQwHF9m .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-cmqPD7HDynQwHF9m .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-cmqPD7HDynQwHF9m .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-cmqPD7HDynQwHF9m .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-cmqPD7HDynQwHF9m .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-cmqPD7HDynQwHF9m .marker{fill:#333333;stroke:#333333;}#mermaid-svg-cmqPD7HDynQwHF9m .marker.cross{stroke:#333333;}#mermaid-svg-cmqPD7HDynQwHF9m svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-cmqPD7HDynQwHF9m p{margin:0;}#mermaid-svg-cmqPD7HDynQwHF9m .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-cmqPD7HDynQwHF9m .cluster-label text{fill:#333;}#mermaid-svg-cmqPD7HDynQwHF9m .cluster-label span{color:#333;}#mermaid-svg-cmqPD7HDynQwHF9m .cluster-label span p{background-color:transparent;}#mermaid-svg-cmqPD7HDynQwHF9m .label text,#mermaid-svg-cmqPD7HDynQwHF9m span{fill:#333;color:#333;}#mermaid-svg-cmqPD7HDynQwHF9m .node rect,#mermaid-svg-cmqPD7HDynQwHF9m .node circle,#mermaid-svg-cmqPD7HDynQwHF9m .node ellipse,#mermaid-svg-cmqPD7HDynQwHF9m .node polygon,#mermaid-svg-cmqPD7HDynQwHF9m .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-cmqPD7HDynQwHF9m .rough-node .label text,#mermaid-svg-cmqPD7HDynQwHF9m .node .label text,#mermaid-svg-cmqPD7HDynQwHF9m .image-shape .label,#mermaid-svg-cmqPD7HDynQwHF9m .icon-shape .label{text-anchor:middle;}#mermaid-svg-cmqPD7HDynQwHF9m .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-cmqPD7HDynQwHF9m .rough-node .label,#mermaid-svg-cmqPD7HDynQwHF9m .node .label,#mermaid-svg-cmqPD7HDynQwHF9m .image-shape .label,#mermaid-svg-cmqPD7HDynQwHF9m .icon-shape .label{text-align:center;}#mermaid-svg-cmqPD7HDynQwHF9m .node.clickable{cursor:pointer;}#mermaid-svg-cmqPD7HDynQwHF9m .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-cmqPD7HDynQwHF9m .arrowheadPath{fill:#333333;}#mermaid-svg-cmqPD7HDynQwHF9m .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-cmqPD7HDynQwHF9m .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-cmqPD7HDynQwHF9m .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cmqPD7HDynQwHF9m .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-cmqPD7HDynQwHF9m .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cmqPD7HDynQwHF9m .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-cmqPD7HDynQwHF9m .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-cmqPD7HDynQwHF9m .cluster text{fill:#333;}#mermaid-svg-cmqPD7HDynQwHF9m .cluster span{color:#333;}#mermaid-svg-cmqPD7HDynQwHF9m div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-cmqPD7HDynQwHF9m .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-cmqPD7HDynQwHF9m rect.text{fill:none;stroke-width:0;}#mermaid-svg-cmqPD7HDynQwHF9m .icon-shape,#mermaid-svg-cmqPD7HDynQwHF9m .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-cmqPD7HDynQwHF9m .icon-shape p,#mermaid-svg-cmqPD7HDynQwHF9m .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-cmqPD7HDynQwHF9m .icon-shape .label rect,#mermaid-svg-cmqPD7HDynQwHF9m .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-cmqPD7HDynQwHF9m .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-cmqPD7HDynQwHF9m .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-cmqPD7HDynQwHF9m :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 优化后视频
I 帧间隔 6
最多解码 5 帧
无 B 帧单向解码
零延迟响应
常规视频
I 帧间隔 250+
拖动时回溯解码
卡顿 / 发热
四、前端实现:手势映射到视频时间轴
底层地基用 ffmpeg 夯实后,前端代码变得极其清爽,甚至不需要引入任何第三方库。
只需要监听 touchstart 和 touchmove 事件,计算滑动距离比例,等比映射到视频的 duration 即可:
javascript
// 前端 3D 视频控制逻辑
const video = document.getElementById('product-360-video');
let startX = 0;
let startProgress = 0;
// 阻止默认的滚屏行为,专心处理左右摩擦
const handleTouchStart = (e) => {
startX = e.touches[0].clientX;
// 记录开始滑动时视频的当前进度比例
startProgress = video.currentTime / video.duration;
};
const handleTouchMove = (e) => {
if (!video.duration) return;
const currentX = e.touches[0].clientX;
const deltaX = currentX - startX;
// 视口宽度对应的最大滑动距离,可根据手感调整阻尼系数
const swipeRange = window.innerWidth * 1.5;
const progressDelta = deltaX / swipeRange;
// 计算最新进度,做好边界处理(首尾循环相连)
let nextProgress = startProgress - progressDelta;
if (nextProgress < 0) nextProgress += 1;
if (nextProgress > 1) nextProgress -= 1;
// 精确跳转到目标帧时间,由于 -bf 0 和低 GOP 限制,这次跳转将是 0 延迟的
video.currentTime = nextProgress * video.duration;
};
video.addEventListener('touchstart', handleTouchStart, { passive: true });
video.addEventListener('touchmove', handleTouchMove, { passive: false });
关键设计点
#mermaid-svg-HOgtOr5FOKYvgQbA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HOgtOr5FOKYvgQbA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HOgtOr5FOKYvgQbA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HOgtOr5FOKYvgQbA .error-icon{fill:#552222;}#mermaid-svg-HOgtOr5FOKYvgQbA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HOgtOr5FOKYvgQbA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HOgtOr5FOKYvgQbA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HOgtOr5FOKYvgQbA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HOgtOr5FOKYvgQbA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HOgtOr5FOKYvgQbA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HOgtOr5FOKYvgQbA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HOgtOr5FOKYvgQbA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HOgtOr5FOKYvgQbA .marker.cross{stroke:#333333;}#mermaid-svg-HOgtOr5FOKYvgQbA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HOgtOr5FOKYvgQbA p{margin:0;}#mermaid-svg-HOgtOr5FOKYvgQbA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HOgtOr5FOKYvgQbA .cluster-label text{fill:#333;}#mermaid-svg-HOgtOr5FOKYvgQbA .cluster-label span{color:#333;}#mermaid-svg-HOgtOr5FOKYvgQbA .cluster-label span p{background-color:transparent;}#mermaid-svg-HOgtOr5FOKYvgQbA .label text,#mermaid-svg-HOgtOr5FOKYvgQbA span{fill:#333;color:#333;}#mermaid-svg-HOgtOr5FOKYvgQbA .node rect,#mermaid-svg-HOgtOr5FOKYvgQbA .node circle,#mermaid-svg-HOgtOr5FOKYvgQbA .node ellipse,#mermaid-svg-HOgtOr5FOKYvgQbA .node polygon,#mermaid-svg-HOgtOr5FOKYvgQbA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HOgtOr5FOKYvgQbA .rough-node .label text,#mermaid-svg-HOgtOr5FOKYvgQbA .node .label text,#mermaid-svg-HOgtOr5FOKYvgQbA .image-shape .label,#mermaid-svg-HOgtOr5FOKYvgQbA .icon-shape .label{text-anchor:middle;}#mermaid-svg-HOgtOr5FOKYvgQbA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HOgtOr5FOKYvgQbA .rough-node .label,#mermaid-svg-HOgtOr5FOKYvgQbA .node .label,#mermaid-svg-HOgtOr5FOKYvgQbA .image-shape .label,#mermaid-svg-HOgtOr5FOKYvgQbA .icon-shape .label{text-align:center;}#mermaid-svg-HOgtOr5FOKYvgQbA .node.clickable{cursor:pointer;}#mermaid-svg-HOgtOr5FOKYvgQbA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HOgtOr5FOKYvgQbA .arrowheadPath{fill:#333333;}#mermaid-svg-HOgtOr5FOKYvgQbA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HOgtOr5FOKYvgQbA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HOgtOr5FOKYvgQbA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HOgtOr5FOKYvgQbA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HOgtOr5FOKYvgQbA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HOgtOr5FOKYvgQbA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HOgtOr5FOKYvgQbA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HOgtOr5FOKYvgQbA .cluster text{fill:#333;}#mermaid-svg-HOgtOr5FOKYvgQbA .cluster span{color:#333;}#mermaid-svg-HOgtOr5FOKYvgQbA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HOgtOr5FOKYvgQbA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HOgtOr5FOKYvgQbA rect.text{fill:none;stroke-width:0;}#mermaid-svg-HOgtOr5FOKYvgQbA .icon-shape,#mermaid-svg-HOgtOr5FOKYvgQbA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HOgtOr5FOKYvgQbA .icon-shape p,#mermaid-svg-HOgtOr5FOKYvgQbA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HOgtOr5FOKYvgQbA .icon-shape .label rect,#mermaid-svg-HOgtOr5FOKYvgQbA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HOgtOr5FOKYvgQbA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HOgtOr5FOKYvgQbA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HOgtOr5FOKYvgQbA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} touchstart
记录起始 X 坐标
记录当前视频进度
touchmove
计算滑动位移 deltaX
映射为进度增量 progressDelta
计算目标进度 nextProgress
边界循环处理
设置 video.currentTime
✅ 0 延迟响应
| 设计点 | 说明 |
|---|---|
passive: true(touchstart) |
不阻止默认行为,保证浏览器滚动优化 |
passive: false(touchmove) |
允许阻止默认滚动,专注处理横向滑动 |
swipeRange |
控制滑动灵敏度,window.innerWidth * 1.5 是经过调优的手感值 |
| 首尾循环 | 保证用户往一个方向持续滑动时,商品能无缝循环旋转 |
| 进度比例记录 | 避免每次 touchmove 都重新计算,只累加增量 |
五、方案对比总结
| 方案 | 加载体积 | 移动端性能 | 画质 | 实现复杂度 |
|---|---|---|---|---|
| Sprite 拼图 | 20MB+ | ❌ 闪烁/卡死 | 中 | 中 |
| WebGL 渲染 | 模型文件 | ❌ 发热/掉帧 | 廉价塑料感 | 高 |
| 视频 Scrubbing(优化后) | 800KB | ✅ 丝滑 | 电影级 | 低 |
这套方案的核心洞察是:用离线渲染保证画质,用编码参数保证性能,用前端手势映射保证交互。三条路各自解决了单一维度的问题,但只有把它们串起来,才能在移动端同时做到「画质好 + 加载快 + 操作丝滑」。
关键的一句话 :-g 6 -bf 0 这两个参数,是移动端视频拖拽体验的分水岭。