手机知网移动端的性能优化实践
一、为什么移动端要单独谈
手机知网承载了文献检索、在线阅读、会员与个性化推荐等多条业务线,用户设备从旗舰机到百元机跨度极大,网络从 5G 到 2G 弱网并存。桌面端的优化经验在移动端会”打折”,原因有三:
- 主线程更弱:同样的 JS 在千元机上执行时间是旗舰机的 3~5 倍,长任务更容易掉帧。
- 流量敏感:用户会真的在意包大小,一个 2MB 的 JS 在弱网下要十几秒。
- 弱网普遍:首屏瓶颈往往不是带宽而是 RTT(往返时延)——一次握手、一个重定向就能吃掉几百毫秒。
我们的优化目标非常具体:首屏可交互时间(TTI)< 2.5s,核心 Web 指标(LCP / INP / CLS)全面达标,且在 4G 中端机上可复现。
二、先讲指标:你在优化什么
不定义指标,优化就是盲人摸象。手机知网重点盯四个:
| 指标 | 含义 | 达标线 | 我们关注点 |
|---|---|---|---|
| LCP | 最大内容绘制 | < 2.5s | 首屏文献卡片/封面 |
| INP | 交互到下次绘制 | < 200ms | 搜索框输入、列表滚动 |
| CLS | 累积布局偏移 | < 0.1 | 图片/广告加载后的跳变 |
| TTFB | 首字节时间 | < 800ms | CDN 与接口链路 |
INP 是 2024 年取代 FID 的新指标,更贴近”用户觉得卡不卡”,我们为此专门治理长任务。
三、首屏优化:少即是快
3.1 关键 CSS 内联
首屏需要的样式(布局骨架、品牌色、字体栈)直接内联进 HTML <head>,非首屏样式(@media print、阅读器样式)延迟加载,避免一次额外的 CSS 请求阻塞首次渲染。
<head>
<style>/* 仅首屏 ~14KB 关键样式 */</style>
<link rel="preload" href="/full.css" as="style" onload="this.rel='stylesheet'">
</head>
3.2 字体优化
Web 字体是隐形性能杀手。我们:
- 设置
font-display: swap,先显示系统字体再替换,杜绝”字体阻塞白屏”; - 仅引入实际用到的字重(400/500/700),不打包整个字族;
- 中文用子集化(subset)+
unicode-range分段加载,首屏只拉当前页面字符。
3.3 骨架屏替代菊花图
在真实数据到达前用与真实布局同构的骨架屏给出结构预期,既降低感知等待,又通过”占位即最终尺寸”规避 CLS。
四、图片优化:移动端的重头戏
文献封面、期刊大图是体量最大的资源。我们的组合拳:
- 格式:优先 AVIF,回退 WebP,再回退 JPEG;同等质量 AVIF 体积约为 JPEG 的 1/4。
- 响应式
srcset:按设备 DPR 与视口给不同尺寸,百元机不下载 2x 图。
<img
src="cover-400.avif"
srcset="cover-200.avif 200w, cover-400.avif 400w, cover-800.avif 800w"
sizes="(max-width: 600px) 200px, 400px"
loading="lazy" decoding="async" alt="文献封面"
/>
- LQIP/Blurhash 占位:图到达前先铺模糊底图,到达后淡入,体验连续。
- CDN + 边缘裁剪:图片在边缘节点按请求尺寸实时裁剪,源站不存多份。
五、资源:按路由切割与 Tree Shaking
手机知网采用路由级代码分割,进入”阅读器”才加载阅读引擎,进入”会员中心”才加载会员逻辑——首页不该为它们买单。
const Reader = lazy(() => import('./Reader'));
// Suspense 兜底,避免阅读器 chunk 下载时整页白屏
<Suspense fallback={<ReaderSkeleton />}>
<Reader docId={id} />
</Suspense>
同时严格 Tree Shaking:不引入”整包 lodash”,只 import debounce from 'lodash/debounce';日期库从 moment(70KB)迁到 day.js(2KB)。
六、运行时优化:别让主线程”憋气”
6.1 长任务拆分
一次渲染 500 条搜索结果是典型长任务。我们把它切片,用 requestIdleCallback / setTimeout(0) 分帧执行,保证滚动手势不被饿死。
function chunkRender(items: Item[], render: (b: Item[]) => void) {
let i = 0;
const step = () => {
const batch = items.slice(i, i + 50);
render(batch);
i += 50;
if (i < items.length) (requestIdleCallback || setTimeout)(step);
};
step();
}
6.2 重计算移出主线程
文献的本地排序、关键词高亮、去重等纯计算放到 Web Worker,主线程只负责渲染,INP 显著下降。
七、离线:Service Worker 兜底
针对地铁、电梯等断网瞬间,我们用 Service Worker 做静态资源缓存 + 接口读缓存:
self.addEventListener('fetch', (e) => {
if (e.request.method !== 'GET') return;
e.respondWith(
caches.match(e.request).then((hit) =>
hit || fetch(e.request).then((res) => {
const copy = res.clone();
caches.open('cnki-v1').then((c) => c.put(e.request, copy));
return res;
})
)
);
});
缓存策略采用 stale-while-revalidate:先用旧数据秒开,后台静默更新。检索历史、收藏与标注写入 IndexedDB,信号恢复后自动同步,体验连续不中断。
八、弱网适配:优雅降级
弱网下我们主动收敛:
- 接口超时从 10s 降到 4s,超时即降级为”缓存结果 + 重试提示”;
- 封面图降级为纯色占位,不阻塞文字阅读;
- 复杂动效自动关闭,避免低端机掉帧。
九、低端机友好
- 对
prefers-reduced-motion用户关闭非必要动画; deviceMemory < 4或hardwareConcurrency <= 4时,图表降级为静态图、虚拟列表 overscan 调小;- 列表滚动暂停图片解码,优先保住滚动帧。
十、监控体系:性能预算
优化要可持续,必须量化:
- RUM 真实埋点:采集每位用户的 LCP/INP/CLS 分布,按机型、网络分桶;
- 性能预算(Performance Budget):CI 里设 JS 体积上限、LCP 阈值,超标阻断合并;
- 回归对比:每次发版自动对比核心指标,劣化超 5% 自动告警。
十一、实战:阅读器首屏 4s → 1.8s 的改造路径
某版本阅读器首屏 4s,用户流失明显。我们按”指标→归因→治理”推进:
- 归因:LCP 是被懒加载的阅读器壳阻塞,TTFB 正常;
- 治理:壳代码内联关键路径、阅读引擎改按需加载、封面走 AVIF + CDN 裁剪;
- 结果:LCP 降到 1.8s,INP 从 280ms 降到 150ms,阅读完成率提升 12%。
十二、小结
移动端优化的本质是尊重约束:尊重网络、尊重设备、尊重用户流量。它不是”锦上添花”,而是可用性本身。手机知网的性能工作仍在持续——下一期我们聊”阅读器内核如何做到秒开百兆 PDF”,敬请关注。