手机知网移动端的性能优化实践

一、为什么移动端要单独谈

手机知网承载了文献检索、在线阅读、会员与个性化推荐等多条业务线,用户设备从旗舰机到百元机跨度极大,网络从 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首字节时间< 800msCDN 与接口链路

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 < 4hardwareConcurrency <= 4 时,图表降级为静态图、虚拟列表 overscan 调小;
  • 列表滚动暂停图片解码,优先保住滚动帧。

十、监控体系:性能预算

优化要可持续,必须量化:

  • RUM 真实埋点:采集每位用户的 LCP/INP/CLS 分布,按机型、网络分桶;
  • 性能预算(Performance Budget):CI 里设 JS 体积上限、LCP 阈值,超标阻断合并;
  • 回归对比:每次发版自动对比核心指标,劣化超 5% 自动告警。

十一、实战:阅读器首屏 4s → 1.8s 的改造路径

某版本阅读器首屏 4s,用户流失明显。我们按”指标→归因→治理”推进:

  1. 归因:LCP 是被懒加载的阅读器壳阻塞,TTFB 正常;
  2. 治理:壳代码内联关键路径、阅读引擎改按需加载、封面走 AVIF + CDN 裁剪;
  3. 结果:LCP 降到 1.8s,INP 从 280ms 降到 150ms,阅读完成率提升 12%。

十二、小结

移动端优化的本质是尊重约束:尊重网络、尊重设备、尊重用户流量。它不是”锦上添花”,而是可用性本身。手机知网的性能工作仍在持续——下一期我们聊”阅读器内核如何做到秒开百兆 PDF”,敬请关注。

← 返回博客列表