怎么用 这是格式与深度的参考,不是让你照抄的模板。数字全部是示例,直接抄进简历一问就穿——必须换成你自己的真实数据,而且要能说清是怎么测出来的。下面每个模块都有「数字是怎么测的」和「面试追问」两段,写不出那两段的模块就不要往简历上写

面试题库(电商 / 短视频 / 社区 / 本地生活 / 旅游 / 企业服务 多业务场景)

项目背景设定 国际短视频社区 App,uni-app 一套代码发小程序和 App 两端。核心场景是上下滑的视频信息流图文笔记发布私信。用户分布在多个国家,网络条件差异大,机型跨度从旗舰到低端千元机。
为什么选这个背景 移动端的含金量集中在性能与资源约束上——内存、帧率、弱网、电量,这些都是能测出数字、也能讲出取舍的硬问题,比堆功能列表有说服力得多。

模块一:信息流列表与播放性能

  1. 信息流列表与播放性能(窗口复用 + 分级预加载 + 实例数控制)★★★
    简历这样写 信息流列表与播放性能优化(uni-app + swiper + IntersectionObserver + video 组件):把「全量渲染 + 隐藏非当前项」改为固定 3 个实例的窗口复用,滑出窗口即销毁并释放播放器;按「封面图 → 下一条前几秒 → 完整视频」分级预加载,用 IntersectionObserver 在渲染层判断可见性替代滚动回调计算。低端机连续滑动 100 条后内存由约 610MB 降至 350MB 上下,不再触发系统回收;滑动平均帧率由 43 提升到 54
    展开完整拆解
    为什么要这么设计

    第一版的实现是最直观的写法:把接口返回的数据全部渲染成列表,用 swiper 上下滑,非当前项用 v-show 隐藏。功能上没问题,但在测试机(一台千元机)上滑到三四十条就开始卡,滑到七八十条应用直接被系统杀掉。

    定位之后发现三个问题叠加在一起。一是 v-show 只是隐藏,节点和视频实例都还在——滑过 50 条就有 50 个 video 组件活着,而原生视频组件本身有数量上限,超了之后新的视频压根播不出来。二是每次滚动都在算当前是第几项,滚动事件在小程序里是跨线程回调,高频触发时通信开销把主线程占满。三是预加载太激进,一次预载了后面三条的完整视频,带宽和解码资源被抢光。

    所以三个方向分别改:渲染层做窗口复用,可见性判断挪到渲染层,预加载改成分级

    整体链路
    数据层 渲染窗口(固定 3 个实例) 资源 │ │ │ ├─ 首屏拉 10 条 ────────────────▶ [ prev | current | next ] │ │ │ ↑ ↑ ↑ │ │ │ 暂停+ 播放 预载前 │ │ │ 释放 几秒 │ │ │ │ │ 滑到倒数第 3 条 ──▶ 拉下一页 │ 窗口整体前移 │ │ (按 ID 去重后追加) │ 滑出的实例 destroy ├─ 释放播放器 │ │ │ │ 数据数组同步裁剪 │ IntersectionObserver │ │ (只保留窗口附近约 20 条) │ 在渲染层判断当前项 │ └──────────────────────────────┴─────────────────────────────┘ 预加载分级:封面图(立即)→ 下一条视频前 3~5 秒(进入窗口时)→ 完整视频(开始播放后)

    要点是数据和视图都要裁剪。只做视图层的虚拟化不够——用户刷了两百条,数据数组里就有两百条对象,每条带封面 URL、作者信息、统计数据,这部分内存同样可观。

    分步拆解
    1. 窗口复用。不管数据有几百条,同时只渲染当前项加前后各一项,一共 3 个。滑动时窗口整体前移,滑出的实例销毁而不是隐藏。为什么是 3 个而不是 5 个:3 个刚好覆盖「当前 + 快速滑动时的下一项 + 回退时的上一项」,再多只是增加内存占用,实测收益很小。
    2. 播放器资源必须显式释放。只销毁组件不够,要拿到播放器上下文调用暂停和销毁。这一步漏了会出现两个典型 bug:快速滑动时听到两个视频的声音重叠,以及滑了几十条后新视频播不出来(实例数超上限)。
    3. 可见性判断用 IntersectionObserver它在渲染层判断元素是否进入视口,不需要把滚动位置回传到逻辑层再计算。小程序的双线程架构下,这个改动省掉了高频的跨线程通信,是帧率提升的主要来源。
    4. 预加载分三级。封面图立刻加载(保证切过去不是黑屏);下一条视频只预载前几秒(能立刻起播就够,不整段预载);完整视频等真正开始播放后再继续拉。国际业务的带宽成本和网络稳定性都不允许整段预载。
    5. 清晰度按实测网速选。首屏用较低码率保证能起播,起播后无感切到高清。「等三秒看高清」的体验远差于「立刻看到略糊的画面」,这是短视频的核心体验判断。
    6. 数据去重与裁剪。翻页时后端推荐结果可能重复,按视频 ID 去重;同时把窗口之外较远的数据从数组里移除,只保留前后约 20 条。用户回滑超出范围的情况极少,真发生了重新拉一次可以接受。
    关键决策与取舍

    为什么不用现成的虚拟列表组件。试过社区的虚拟列表方案,但短视频场景有个特殊性——一屏只有一项、且必须整屏对齐,通用虚拟列表要处理不定高和任意滚动位置,复杂度和开销都是浪费。用 swiper 加固定 3 项的窗口反而更简单可控,代码量少一半。通用方案不一定适合特化场景,这个判断要主动说出来。

    裁剪数据的代价。裁掉之后用户往回滑超过 20 条就要重新请求,会有短暂加载。权衡的结果是接受这个代价——因为回滑很远的行为占比极低,而内存问题是必须解决的。如果面试官追问,我会说更好的做法是把裁掉的数据缓存到本地存储而不是直接丢弃,这是当时没做但应该做的。

    踩过的坑:小程序端和 App 端表现不一致。同一套代码在 App 端(WebView 渲染)流畅,小程序端卡顿明显,因为小程序的原生视频组件层级和 setData 机制都不同。最后是用条件编译在两端走了略微不同的实例数和预加载策略。这也印证了跨端「大部分代码复用,剩下的差异必须正面处理」。

    数字是怎么测的

    内存:Android 上用 adb shell dumpsys meminfo <包名>TOTAL PSS,固定操作路径——连续滑动 100 条(用脚本或手动匀速滑),每滑 20 条记一次。改造前是持续上涨到 610MB 左右然后被系统回收;改造后稳定在 350MB 上下不再增长。关键是「稳定不增长」而不是绝对值,因为绝对值和机型、系统版本都有关。

    帧率:用小程序开发者工具的性能面板,或者 App 端用 adb shell dumpsys gfxinfo 看掉帧。取滑动过程的平均帧率。为什么是 54 不是 60:真实设备上视频解码加渲染很难跑满帧,说 60 反而不可信。

    测试机型要说清。「低端机」要能报出具体型号或芯片档次,因为同样的代码在旗舰机上根本测不出问题——这也是为什么性能优化必须用低端机验证。

    面试追问
    Q:为什么内存会一直涨?video 组件被隐藏了不是应该被回收吗? A:v-show 只是把 CSS 的 display 设成 none,节点还在树上、组件实例还活着、播放器上下文也没释放。真正要回收得让组件从树上移除(v-if 或者从数据源里删掉),并且显式调用播放器的销毁方法。另外视频的解码缓冲区占的是原生内存,不在 JS 堆里,所以看 JS 内存是发现不了的,必须看进程的总内存。
    Q:为什么用 IntersectionObserver 比滚动事件快? A:小程序是双线程架构,逻辑层和渲染层分开。滚动事件要把位置信息从渲染层跨线程传到逻辑层,逻辑层算完再把结果传回去,高频触发时这个通信开销很大。IntersectionObserver 的判断在渲染层完成,只在「进入/离开视口」这个状态变化时才回调一次逻辑层,通信次数从每帧几十次降到状态变化时的一两次。
    Q:只预载几秒,用户快速滑动时不还是要等吗? A:会,但比整段预载好。整段预载在快速滑动时更糟——用户滑过三条,前两条的完整视频都在下载,把带宽全占了,第三条反而更慢。只预载前几秒的话,每条的起播数据量小,快速滑动时也能跟上。另外配合封面图预载,视觉上至少不是黑屏。真要进一步优化,可以根据滑动速度动态调整——快速滑动时只预载封面,停下来才预载视频。
    Q:这个模块你觉得还有什么没做好? A:两点。一是裁掉的数据直接丢了没缓存,用户回滑要重新请求,应该写到本地存储。二是没做播放器实例池——现在是销毁再创建,如果改成复用同一批实例只切换数据源,创建开销还能再省一些,但要处理好状态重置,否则会出现「切过去还在播上一条」的问题。

模块二:视频上传(分片与断点续传)

  1. 视频上传(本地压缩 + 分片并发 + 断点续传)★★★
    简历这样写 视频上传链路(uni-app + 客户端直传对象存储 + 分片上传 + 本地压缩):客户端凭时效凭证直传对象存储,业务服务不承载文件流量;实现分片并发上传(并发数受控)、失败单片重试断点续传(以服务端已收分片列表为准),并在上传前做本地压缩。上传体积由平均约 35MB 降至 6MB 上下,弱网(限速 256KB/s)下单次上传成功率由约 五成提升到九成左右
    展开完整拆解
    为什么要这么设计

    最早是最简单的实现:选完视频整个文件一次性传给自己的服务器,服务器再转存到对象存储。三个问题。

    一是成功率极低。手机拍的视频动辄几十兆,用户在地铁上、在国外的弱网环境下,一次传完的概率很低,而且失败了要整个重来。二是服务器成了瓶颈,文件流量全走业务服务器,带宽和内存都吃不消。三是没有进度和恢复能力,用户传到一半切个后台回来就得重传。

    所以改造的三个方向:不让文件经过自己的服务器(直传对象存储)、把大文件拆小(分片加断点续传)、从源头减少体积(本地压缩)。其中本地压缩是投入产出比最高的一步——它直接把问题规模降了一个数量级,比任何传输层的优化都有效。

    整体链路
    客户端 业务服务 对象存储 │ │ │ ├─ 选择视频 + 本地校验(大小/时长/格式) │ ├─ 本地压缩(降码率与分辨率) │ │ │ │ │ ├─ 申请上传凭证 ──────────────────▶ │ 签发时效凭证 │ │ │ (限路径/大小/类型) │ │ ◀─ uploadId + 凭证 ───────────── │ │ │ │ │ ├─ 查已收分片列表 ───────────────────────────────────▶ │ │ ◀─ 已收 [1,2,5] ────────────────────────────────── │ ├─ 只传缺失分片(并发 3,失败单片重试)──────────────────▶ │ │ │ │ ├─ 通知合并 ──────────────────────▶ │ ──────────────────▶ │ 合并 │ │ 创建转码任务 │ │ ◀─ 轮询转码状态 ───────────────── │ ◀── 转码完成 ────── │ └─ 发布 │ │ 本地记录:uploadId + 已完成分片索引 → 中断后恢复用(但以服务端返回为准)
    分步拆解
    1. 上传前必须本地压缩。按最大边长和码率参数压到几百 KB 到几兆,肉眼几乎无差别。这一步把后面所有问题都简化了——传的数据少一个数量级,失败率、耗时、流量成本同时下降。
    2. 客户端直传对象存储。业务服务只签发有时效和权限限制的凭证(限定路径前缀、文件大小上限、内容类型、有效期),不碰文件流。凭证的限制必须严,否则会被当免费网盘滥用。
    3. 分片大小和并发数要调。分片太小则请求数多、握手开销占比高;太大则单片失败重传代价高。弱网环境倾向小一点。并发数控制在 3 个左右——小程序有并发请求上限,设太高会有请求直接失败,而且这个失败不是网络错误,很难排查。
    4. 断点续传以服务端为准。本地记录 uploadId 和已完成的分片索引,但恢复时必须先问服务端「哪些分片已经收到了」,然后只传缺的。因为本地记为成功的分片可能实际没落盘,信本地记录会导致合并时缺片。
    5. 凭证过期要能续期。大文件上传时间长,凭证可能中途过期导致后续分片全部 403。要检测这个错误并自动重新申请凭证继续传,而不是让整个任务失败。这是大文件上传的经典坑。
    6. 转码是独立的异步阶段。上传完成不等于可播放。前端要展示「上传完成,正在处理」并轮询转码状态,转码失败要给出可理解的原因(格式不支持、时长超限),而不是笼统的失败。
    关键决策与取舍

    压缩参数怎么定。压太狠画质肉眼可见地糊,用户会不满;压太轻起不到作用。做法是先用几个典型视频(室内、室外、快速运动)试不同参数,在可接受画质的下限处取值。这里的取舍是明确的:宁可画质略降也要保证传得上去——传不上去的视频画质再好也没有意义。

    为什么不在服务端压缩。服务端压缩画质控制更好,但那意味着原始大文件还是要传上来,压根没解决核心问题。所以本地压缩是必须的,服务端转码是在此基础上再出多档清晰度。

    小程序端的硬限制要认。小程序对单个文件大小有上限,超过的压根传不了。这种情况只能引导用户用 App 或者提示降低视频时长,技术上绕不过去。承认平台限制、给出产品层面的降级路径,比硬找技术方案更专业。

    数字是怎么测的

    上传体积:取一批真实用户视频(或自己拍的典型样本,覆盖不同时长和场景),统计压缩前后的平均文件大小。样本量要说得出来(比如「30 个样本」),不然「平均」没有意义。

    弱网成功率:用抓包工具或系统的网络限速功能把带宽限到 256KB/s,同一批视频各传 20 次,统计成功次数。为什么说「约五成」和「九成左右」而不是精确百分比:样本量只有几十次,算出 52.5% 这种精度是假的,用模糊量级更诚实。

    被追问时要能说清失败的原因分布——改造前的失败主要是整体超时和中途断网,改造后残留的失败主要是压根传不动的极端弱网。能报出原因分布,说明你真的分析过而不是只看了个成功率。

    面试追问
    Q:为什么断点续传要以服务端返回的分片列表为准,本地记录不行吗? A:本地记录的是「我发出去并收到了成功响应」,但存在几种偏差:响应丢在回来的路上(实际成功但本地记为失败,这种只是多传一次不严重);更危险的是响应成功但服务端后续处理失败、或者分片在服务端被清理了(临时分片通常有过期时间),这时本地记为成功但实际缺片,合并时才报错,而那时候用户已经等了很久。所以每次恢复都问一次服务端,多一次请求换取可靠性。
    Q:分片并发数为什么是 3?怎么确定的? A:一是小程序的并发请求上限约束,要给其他业务请求留余量;二是实测——并发从 1 加到 3 时总耗时明显下降,再往上加提升很小,而弱网下失败率反而上升(并发太多互相抢带宽,每个都变慢,更容易超时)。所以 3 是实测的性价比拐点,不是拍的。
    Q:秒传怎么实现?你做了吗? A:思路是上传前算文件哈希问服务端有没有相同文件,有就直接关联不用传。我没做,原因是本地压缩之后文件哈希不稳定——同一个视频在不同机型上压缩出的文件字节不同,哈希也不同,秒传命中率会很低。如果要做,得对压缩前的原始文件算哈希,但那要读整个大文件计算,在低端机上本身就耗时耗电。所以这个功能的收益要看业务场景——搬运内容多的平台值得做,原创为主的平台命中率低。
    Q:用户上传到一半退出应用,回来怎么继续? A:uploadId 和已完成分片索引存本地,重新进入发布页时检测到有未完成的任务就提示「有一个未完成的上传,是否继续」。App 端可以做后台上传(要处理系统的后台执行时长限制),小程序端切后台会被挂起,只能等用户回来继续。这里有个体验细节:退出页面前要提示「上传中,确定离开吗」,避免用户误操作。

模块三:私信消息可靠性与长列表

  1. 私信消息可靠性与长列表(序列号增量同步 + 本地存储 + 虚拟滚动)★★★
    简历这样写 私信模块的消息可靠性与长列表优化(uni-app + WebSocket + 本地存储 + 虚拟滚动):以会话内单调递增的序列号为基准做增量同步,推送只作为「有新消息」的信号、实际内容按本地最大序列号拉增量,缺号自动补拉;消息发送采用本地回显 + 客户端唯一 ID 去重,重发不产生重复。进入会话首屏渲染由约 700ms 降至 150ms 上下(本地已有历史时),压测下消息重复插入由每轮可复现变为未再复现
    展开完整拆解
    为什么要这么设计

    第一版是「纯推模式加每次进会话全量拉取」。问题很快出来:

    一是推送丢了消息就没了。WebSocket 在移动端极不稳定——切后台被断开、网络切换、弱网丢包,任何一次断连期间的消息,如果只依赖推送就永久丢失,用户要杀掉应用重进才能看到。二是多端不一致,手机上读过的消息在小程序端还是未读。三是进会话慢,每次都从服务端拉最近 50 条,弱网下要等一两秒白屏。

    根本原因是把推送当成了数据来源。改造的核心思路是:推送只是信号,数据靠拉;本地是主要数据源,服务端是增量补齐。这个转变之后上面三个问题同时被解决。

    整体链路
    进入会话 │ ├─ 读本地消息 ──▶ 立即渲染(首屏不等网络) │ │ │ └─ 取本地最大序列号 maxSeq │ ├─ 拉增量:GET /messages?after=maxSeq ──▶ 服务端 │ ◀─ 返回 maxSeq 之后的消息 ──────────── ├─ 按 seq 排序插入 + 按消息 ID 去重 ├─ 检测序列号缺口 → 补拉缺失区间 └─ 上报已读位点 收到推送(仅作为信号) └─▶ 触发一次「拉增量」,而不是直接把推送内容插进列表 发送消息 ├─ 生成客户端唯一 ID → 本地回显(状态:发送中) ├─ 提交服务端 → 返回正式 seq ├─ 按客户端 ID 匹配本地那条 → 更新为已发送 └─ 失败 → 标红 + 可重发(重发用同一个客户端 ID,服务端据此去重)
    分步拆解
    1. 序列号是整个方案的地基。服务端给每条消息分配会话内单调递增的 seq。有了它,客户端就能判断「我收到了第 100 条,本地只有到 98,中间缺了 99」,从而主动补拉。没有序列号就没法判断有没有漏消息,只能盲目全量拉。
    2. 本地存储做首屏。进会话先渲染本地已有的消息,不等网络,然后再拉增量补齐。这是首屏耗时从 700ms 降到 150ms 的原因——省掉了一次网络往返。小程序端用 storage,App 端可以用 SQLite 插件存更多历史。
    3. 推送只当信号。收到推送后不直接把内容插进列表,而是触发一次拉增量。这样即使推送丢了,下次任何触发拉取的时机(进会话、切回前台)都能补齐;而且天然解决了推送内容和拉取内容重复的问题。
    4. 发送用本地回显加客户端 ID。点发送立刻在列表显示(状态「发送中」),拿到服务端确认后改成已发送。客户端生成的唯一 ID 有两个作用:服务端据此去重(重发不会产生两条),以及把服务端返回的正式消息和本地那条对应上(否则会出现同一条消息显示两次)。
    5. 未读数本地算。用「会话最大 seq 减去本地已读 seq」计算,不需要每次问服务端。多端已读同步靠服务端推送已读位点。
    6. 长列表用虚拟滚动。聊天记录可能几千条,只渲染可视区附近。向上滚动加载更早的历史时要保持滚动位置不跳——插入内容到顶部会把可视内容顶下去,需要在插入后按内容高度差校正 scrollTop。
    关键决策与取舍

    为什么不用「推模式 + 本地直接插入」。那样首屏更快(推送来了直接显示,不用请求),但可靠性完全依赖推送不丢,而移动端的长连接不可能不丢。用「拉」做数据一致性、用「推」做实时性提醒,是唯一能同时保证快和准的组合。多付出的代价是每次推送要多一次请求,但增量请求很轻。

    本地存储的容量取舍。小程序 storage 有容量上限,不可能存全部历史。做法是只存最近 N 条,更早的按需从服务端拉。N 的取值要平衡首屏效果和容量占用——存太少首屏还是要等网络,存太多挤占其他缓存。

    没做的部分要诚实说。没做端到端加密(业务上没这个要求);没做消息撤回和已读回执(产品优先级不高,而且已读状态在部分海外市场用户接受度不好)。面试时主动说清边界,比让面试官问出来好。

    数字是怎么测的

    首屏渲染耗时:在进入会话的入口和列表首次渲染完成处各打一个时间戳,取差值。要说清测试条件——「本地已有历史时」这个前提必须讲,因为首次进入某个会话本地没数据,还是要等网络,那种情况没有优化空间。这类限定条件不主动说,被问出来就显得心虚。

    消息重复:写脚本模拟弱网下的重复发送和推送重放,检查列表里有没有相同客户端 ID 的两条消息。旧实现(没有客户端 ID 去重)每轮都能复现,改造后没再出现。这和上一个项目里验证并发 bug 的思路一样——可靠性问题用可复现的测试来量化,比用线上统计更实在

    面试追问
    Q:序列号是服务端生成的,那高并发下怎么保证会话内单调递增? A:这是服务端的问题,但要能答上来:按会话维度生成,可以用数据库的自增或者 Redis 的 INCR(key 是会话 ID)。不能用全局递增的 ID(比如雪花算法)当序列号,因为那样会话内的号是跳跃的,客户端没法判断「中间是不是缺了」。这个区分是设计的关键——序列号的作用是连续性检测,不是唯一性。
    Q:如果本地存的数据和服务端不一致怎么办?比如消息被撤回或删除了。 A:这类变更类操作也要走序列号——把「撤回消息 X」本身作为一条新消息下发,客户端拉增量时收到它再去修改本地那条的状态。这样所有变更都在同一条增量流里,天然有序且不会漏。如果撤回不走序列号而是单独推送,就又回到「推送丢了状态就不一致」的老问题。
    Q:向上加载历史消息时保持滚动位置,具体怎么做的? A:插入前记下当前的 scrollHeightscrollTop,插入后新的 scrollHeight 变大了,把 scrollTop 加上高度差,视觉上就没动。小程序里因为拿节点信息是异步的,这个操作要在 nextTick 之后做,而且可能有一帧的跳动。更稳的做法是用倒序布局(列表本身是反向的),插入到「视觉上方」实际是数组末尾,压根不需要校正——但那样其他逻辑要跟着反转,改造成本高。
    Q:这套方案在群聊场景下还适用吗? A:单聊适用,群聊要改。群聊的消息量比单聊大一到两个数量级,而且大群的写扩散成本很高——一条消息要给几千人的收件箱写一份。所以群聊通常改成读扩散:消息只存一份在群里,成员各自维护自己读到的位点。序列号机制还是有用的,但「拉增量」的实现从「查我的收件箱」变成「查群消息表中我的位点之后的部分」。这个差异要能说出来,说明理解了两种扩散模式的适用边界。

没有匹配的内容,换个关键词试试。

项目拆解 · 短视频社区 App(移动端)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据