第1篇:JS 大整数精度问题——Number 的边界与进化方向

刚面了个5年前端工程师,简历上写着精通 JavaScript 核心原理,处理过海量数据场景。我递过水,问了一个让他笑容逐渐凝固的问题。

JS 里 Number 最大安全整数是 2 的 53 次方减 1(即 Number.MAX_SAFE_INTEGER = 9007199254740991)。如果我给你一个后端返回的字符串 ID 9007199254740993,超过了这个值,你怎么处理?

他很快接住:用 BigInt,原生支持大整数,末尾加 n 或用 BigInt() 构造,精度不丢失。我点点头,继续追问。

那好,BigInt 和 Number 不能混算,后端接口大部分还是 JSON 格式,JSON.stringify 碰到 BigInt 直接报错,你打算怎么序列化?前端传给后端时又怎么保证服务端能正确解析?他想了想,可以自定义 replacer 把 BigInt 转成字符串,或者用第三方库,比如 lossless-json

行,那如果业务需要做加减乘除,甚至开根号、幂运算,BigInt 不支持小数,财务场景下要算一个超过 Number 上限的利率,精确到小数点后四位怎么办?有没有比 BigInt 加手动移位更优雅的方案?

他有点紧张了:可以用 decimal 库,或者把小数转成整数再运算。我继续加压——假如你的页面里既有普通 Number 运算,又有 BigInt 运算,还有来自 WebAssembly 的 i64 整数,三种类型在同一个表达式中互操作,你怎么保证类型安全又不损耗性能?IndexedDB、postMessage 传递 BigInt 有什么坑?

他沉默了,额头微微渗汗。

这道题问的是大数处理,本质上考的是你能否看清 JavaScript 数值体系的边界、缺位与进化方向。只会用 BigInt 的人太多,真正能讲透三层防御的很少。

第一层:BigInt 的基础应用与收口策略(及格线)

BigInt 的致命短板:与 Number 混算会抛出 TypeError,Math 对象的方法全部不支持,不能用位运算,JSON.stringify 直接报错。

成熟的做法是在 API 边界做统一收口——在 axios 拦截器里,对后端返回的大整数字段自动转换,同时在 TypeScript 层面用 branded type 严格区分类型,避免混用。

// ✅ TypeScript branded type 区分 BigInt 与 Number
type BigIntId = bigint & { readonly __brand: 'BigIntId' };

function toBigIntId(val: string): BigIntId {
  return BigInt(val) as BigIntId;
}

// ✅ axios 响应拦截器:对白名单字段自动转 BigInt
const BIG_INT_FIELDS = ['id', 'orderId', 'userId', 'amount'];

axios.interceptors.response.use((response) => {
  response.data = transformBigIntFields(response.data, BIG_INT_FIELDS);
  return response;
});

function transformBigIntFields(obj: any, fields: string[]): any {
  if (Array.isArray(obj)) return obj.map(item => transformBigIntFields(item, fields));
  if (obj && typeof obj === 'object') {
    return Object.fromEntries(
      Object.entries(obj).map(([k, v]) => [
        k,
        fields.includes(k) && typeof v === 'string' ? BigInt(v) : transformBigIntFields(v, fields)
      ])
    );
  }
  return obj;
}

// ✅ 自定义 replacer,序列化时把 BigInt 转回字符串
function bigIntReplacer(_key: string, value: any) {
  return typeof value === 'bigint' ? value.toString() : value;
}

const payload = { id: 9007199254740993n, name: '订单' };
JSON.stringify(payload, bigIntReplacer);
// → '{"id":"9007199254740993","name":"订单"}'

// ✅ lossless-json 方案(第三方库,parse 时自动保留大数精度)
import { parse, stringify } from 'lossless-json';
const parsed = parse('{"id":9007199254740993}'); // id 为 LosslessNumber 对象,不丢精度

对于需要小数的超大数(如财务金额),退化为字符串传输,交给后端或专用微服务计算,前端只做展示:

// 财务场景:前端不做超大浮点运算,只格式化展示
function formatLargeAmount(amountStr: string, decimals = 4): string {
  // 用 Intl.NumberFormat 展示,不参与运算
  const num = parseFloat(amountStr);
  return new Intl.NumberFormat('zh-CN', {
    minimumFractionDigits: decimals,
    maximumFractionDigits: decimals,
  }).format(num);
}

第二层:高性能运算与跨类型互操作

BigInt 的性能比 Number 慢,且会频繁触发垃圾回收。对于高频大数运算(链上交易解析、大量订单聚合),可以把热路径运算下沉到 WebAssembly。

⚠️ 注意:Wasm 原生支持 i64/u64,但 128 位整数需要 Rust 侧自行实现分拆,并非 Wasm 规范内置类型。Decimal 提案(TC39 Stage 1)目前仍在早期讨论阶段,2026 年尚主流浏览器原生支持,生产中仍需依赖 decimal.js 等库。

// Rust 侧(编译为 Wasm):u64 加法,通过两个 i32 拼接传递 i64
#[no_mangle]
pub extern "C" fn add_u64(a_lo: u32, a_hi: u32, b_lo: u32, b_hi: u32) -> u64 {
    let a = ((a_hi as u64) << 32) | (a_lo as u64);
    let b = ((b_hi as u64) << 32) | (b_lo as u64);
    a + b
}
// JS 胶水代码:调用 Wasm,将 BigInt 拆分为高低 32 位传入
const wasmModule = await WebAssembly.instantiateStreaming(fetch('/math.wasm'));
const { add_u64 } = wasmModule.instance.exports;

function bigIntToHiLo(n) {
  return { lo: Number(n & 0xFFFFFFFFn), hi: Number(n >> 32n) };
}

const a = 9007199254740993n;
const b = 9007199254740994n;
const { lo: alo, hi: ahi } = bigIntToHiLo(a);
const { lo: blo, hi: bhi } = bigIntToHiLo(b);

// Wasm 返回的是 JS Number(f64),大整数需用 BigInt 重新组合
const result = add_u64(alo, ahi, blo, bhi);
console.log(result); // Wasm 层面精确,JS 侧需注意精度边界

对于需要高精度小数的财务场景,使用成熟的 decimal.js 库:

import Decimal from 'decimal.js';

Decimal.set({ precision: 20, rounding: Decimal.ROUND_HALF_UP });

// 超大利率精确计算
const principal = new Decimal('99999999999999999');
const rate = new Decimal('0.0325');
const interest = principal.times(rate).toDecimalPlaces(4);

console.log(interest.toString()); // '3249999999999999.9675' — 不丢精度

// BigInt 与 Decimal 互操作
function bigIntToDecimal(val: bigint): Decimal {
  return new Decimal(val.toString());
}

关于 IndexedDB 和 postMessage 传递 BigInt 的坑:

// ❌ IndexedDB 不支持直接存储 BigInt(会抛 DataCloneError)
const req = indexedDB.open('db', 1);
req.onsuccess = () => {
  const store = req.result.transaction('ids', 'readwrite').objectStore('ids');
  // store.put(9007199254740993n); // ❌ 报错:DataCloneError
  store.put('9007199254740993');   // ✅ 存字符串
};

// ❌ postMessage 跨域传递 BigInt 同样报 DataCloneError(部分浏览器)
// worker.postMessage(9007199254740993n); // 可能报错

// ✅ 统一在跨上下文边界处序列化为字符串,接收端再还原
worker.postMessage({ bigVal: '9007199254740993' });
// worker 内部:const val = BigInt(data.bigVal);

第三层:端到端的大数治理与可观测性

// ✅ PerformanceObserver 监控大数运算长任务
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 50) {
      console.warn(`[BigInt长任务] ${entry.name}: ${entry.duration.toFixed(2)}ms`);
      reportToMonitor({ name: entry.name, duration: entry.duration });
    }
  }
});
observer.observe({ entryTypes: ['measure'] });

// 包裹大数运算并打点
function trackedBigIntOp(label, fn) {
  performance.mark(`${label}-start`);
  const result = fn();
  performance.mark(`${label}-end`);
  performance.measure(label, `${label}-start`, `${label}-end`);
  return result;
}

const result = trackedBigIntOp('order-id-transform', () => {
  return orderList.map(o => BigInt(o.id));
});

// ✅ 熔断校验:超过业务允许范围直接拒绝
function validateBigIntId(idStr: string): bigint {
  if (!/^\d+$/.test(idStr)) throw new Error('非法 ID 格式');
  if (idStr.length > 20) {
    reportSecurityEvent({ type: 'oversized-id', value: idStr });
    throw new Error('ID 超出业务允许范围');
  }
  return BigInt(idStr);
}

// ✅ 微前端边界:统一通过字符串传递,各应用内部自行还原
// 主应用 → 子应用通信
window.__MICRO_SHARED__ = {
  serializeBigInt: (val: bigint) => val.toString(),
  deserializeBigInt: (val: string) => BigInt(val),
};

第2篇:前端截图方案——不只是调个库那么简单

刚面了一个5年经验的前端工程师,简历写精通 Canvas,熟悉各种截图方案。我问了个很常见的问题:用户在我们的在线设计工具里,想把自己做的海报截图保存,你会怎么实现?

他几乎是秒答:用 html2canvas 把 DOM 转成 canvas,然后导出图片就行了,很多项目都这么干。我点点头,继续问。

那如果海报里有跨域的图片,截图出来是空白,你怎么处理?他愣了一下——配一下 crossorigin 属性,让后端配一下跨域头。好,那如果海报里有自定义字体,html2canvas 渲染出来和真实样式不一样,字间距、行高全乱了,怎么办?他开始犹豫——那先把字体转成图片。

我笑着说:转成图片,那文字就不能二次编辑了。更麻烦的是,假如海报里嵌了一个 Lottie 动画,截图的时候那一帧刚好是模糊的,截出来一片空白,你怎么保证截图内容和用户看到的一致?

他彻底不说话了。

第一层:渲染保真——让截图和屏幕像素级一致

跨域图片同源代理:

// 服务端代理路由(Node.js/Express)
// 前端图片 URL 统一替换为走自己代理的同源地址
app.get('/img-proxy', async (req, res) => {
  const { url } = req.query;
  const response = await fetch(decodeURIComponent(url));
  const buffer = await response.arrayBuffer();
  res.set('Content-Type', response.headers.get('content-type'));
  res.set('Cache-Control', 'public, max-age=86400');
  res.send(Buffer.from(buffer));
});

// 前端:截图前把所有图片 src 替换为代理地址
function proxyImageSrc(element) {
  const imgs = element.querySelectorAll('img[src]');
  imgs.forEach(img => {
    const original = img.src;
    if (!original.startsWith(location.origin)) {
      img.src = `/img-proxy?url=${encodeURIComponent(original)}`;
    }
  });
}

字体加载完成检测后再截图:

// ✅ 用 FontFace API 确保字体真正渲染完毕
async function waitForFonts(fontFamilies) {
  const checks = fontFamilies.map(family =>
    document.fonts.load(`16px "${family}"`)
  );
  await Promise.all(checks);
  // 额外等一帧,让浏览器完成布局重排
  await new Promise(resolve => requestAnimationFrame(resolve));
}

async function captureWithFont(element) {
  await waitForFonts(['思源黑体', 'Source Han Sans']);
  // 此时字体已完全加载,再触发 html2canvas
  const canvas = await html2canvas(element, { useCORS: true });
  return canvas.toDataURL('image/png');
}

动画冻结帧:

// ✅ Lottie 动画暂停到当前帧再截图
async function captureWithAnimation(lottieInstance, element) {
  // 暂停动画,锁定当前帧
  lottieInstance.pause();

  // 等一帧确保 canvas 已更新
  await new Promise(resolve => requestAnimationFrame(resolve));

  const canvas = await html2canvas(element);
  const dataUrl = canvas.toDataURL('image/png');

  // 截图完成后恢复播放
  lottieInstance.play();
  return dataUrl;
}

// ✅ CSS 动画暂停
function freezeCSSAnimations(element) {
  const all = element.querySelectorAll('*');
  all.forEach(el => {
    el.style.animationPlayState = 'paused';
  });
  return () => all.forEach(el => {
    el.style.animationPlayState = '';
  });
}

第二层:性能与稳定性——截图不能把页面卡死

分块渲染 + Web Worker 合成:

// 主线程:把大图分块,交给 Worker 合成
async function captureInChunks(element, chunkHeight = 1000) {
  const { width, height } = element.getBoundingClientRect();
  const chunks = Math.ceil(height / chunkHeight);
  const blobs = [];

  for (let i = 0; i < chunks; i++) {
    const canvas = await html2canvas(element, {
      y: i * chunkHeight,
      height: Math.min(chunkHeight, height - i * chunkHeight),
      width,
    });
    blobs.push(await canvasToBlob(canvas));
    // 主动释放每块 canvas 内存
    canvas.width = 0;
    canvas.height = 0;
  }

  // 发给 Worker 合成
  return new Promise((resolve) => {
    const worker = new Worker('/capture-worker.js');
    worker.postMessage({ blobs, width, height }, blobs.map(b => b));
    worker.onmessage = (e) => resolve(e.data.result);
  });
}

function canvasToBlob(canvas) {
  return new Promise(resolve => canvas.toBlob(resolve, 'image/png'));
}
// capture-worker.js(Worker 内合成)
self.onmessage = async ({ data: { blobs, width, height } }) => {
  const canvas = new OffscreenCanvas(width, height);
  const ctx = canvas.getContext('2d');
  let y = 0;

  for (const blob of blobs) {
    const bitmap = await createImageBitmap(blob);
    ctx.drawImage(bitmap, 0, y);
    y += bitmap.height;
    bitmap.close(); // 释放 ImageBitmap 内存
  }

  const result = await canvas.convertToBlob({ type: 'image/png' });
  self.postMessage({ result }, [result]);
};

内存检测与降级:

// ✅ 检测可用内存,自动降级分辨率
async function captureWithMemoryGuard(element) {
  // navigator.deviceMemory 单位 GB(部分浏览器支持)
  const memoryGB = navigator.deviceMemory ?? 4;
  const scale = memoryGB < 2 ? 1 : window.devicePixelRatio;

  const canvas = await html2canvas(element, { scale });
  const format = memoryGB < 2 ? 'image/jpeg' : 'image/png';
  const quality = memoryGB < 2 ? 0.8 : 1;

  const dataUrl = canvas.toDataURL(format, quality);

  // 立即释放 canvas 内存
  canvas.width = 0;
  canvas.height = 0;

  return dataUrl;
}

超时熔断 + 服务端降级:

async function captureWithFallback(element) {
  const TIMEOUT_MS = 2000;

  const localCapture = html2canvas(element);
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('截图超时')), TIMEOUT_MS)
  );

  try {
    const canvas = await Promise.race([localCapture, timeout]);
    return canvas.toDataURL('image/png');
  } catch (e) {
    console.warn('客户端截图失败,降级为服务端截图', e);
    // 把 DOM 序列化发给服务端渲染
    const html = element.outerHTML;
    const res = await fetch('/api/screenshot', {
      method: 'POST',
      body: JSON.stringify({ html, css: getInlineStyles(element) }),
      headers: { 'Content-Type': 'application/json' },
    });
    return res.json().then(d => d.imageUrl);
  }
}

第三层:可观测与自适应——让截图失败能被发现

// ✅ 截图打点上报
async function captureWithMonitor(element) {
  const start = performance.now();
  let success = false;
  let fallback = false;

  try {
    const result = await captureWithFallback(element);
    success = true;
    return result;
  } catch (e) {
    fallback = true;
    throw e;
  } finally {
    const duration = performance.now() - start;
    reportMetric({
      event: 'screenshot',
      duration,
      success,
      fallback,
      dpr: window.devicePixelRatio,
      memory: navigator.deviceMemory,
    });
  }
}

// ✅ 像素差异一致性校验(开发/灰度环境)
async function validateScreenshotConsistency(element) {
  if (!['development', 'gray'].includes(window.__ENV__)) return;

  const [screenshot, reference] = await Promise.all([
    captureWithFallback(element),
    generateReferenceSnapshot(element), // 用 puppeteer 服务端生成基准图
  ]);

  const diff = await pixelDiff(screenshot, reference);
  if (diff.percentage > 0.01) {
    console.error(`截图与基准差异 ${(diff.percentage * 100).toFixed(2)}%,疑似字体回退`);
    reportInconsistency(diff);
  }
}

第3篇:退出前发送埋点数据——可靠上报系统的三层架构

刚面了一个5年前端,简历写精通前端监控,对埋点 SDK 有深入落地经验。我问他:退出浏览器之前,发送积压的埋点数据请求该如何做?

他脱口而出:用 navigator.sendBeacon,它在页面卸载时异步发送,不阻塞页面关闭,而且本身就是 POST 请求,体积小。我点头——标准答案。

那如果用户直接杀进程、浏览器崩溃,你的 beacon 还发得出去吗?他愣了下:那应该不行,崩溃时监听不到。我继续压——一个电商大促页面,用户连续打开 30 个商品详情,每个详情积压了 50 条交互埋点,退出瞬间要发 1500 条,beacon 单次请求能携带多少数据?

他想了想:不同浏览器限制不太一样,大概 64KB。我说那 1500 条,你准备发多少次?sendBeacon 快速连续调用十次以上浏览器会怎么处理?还有用户点击关闭标签页的那一刻,主线程是否还能执行你的压缩序列化?

他开始沉默,面试到此就结束了。

第一层:底层持久化——让数据不依赖内存,崩溃也能续传

⚠️ 注意:Periodic Background Sync 目前仅 Chrome 支持,且需要用户授予权限并安装 PWA,不是普通网页可以随意使用的 API。

// ✅ 埋点产生后立即写入 IndexedDB,不依赖内存
class PersistentEventQueue {
  constructor(dbName = 'tracker-db') {
    this.dbPromise = this._initDB(dbName);
  }

  async _initDB(name) {
    return new Promise((resolve, reject) => {
      const req = indexedDB.open(name, 1);
      req.onupgradeneeded = () => {
        req.result.createObjectStore('events', {
          keyPath: 'id',
          autoIncrement: true,
        });
      };
      req.onsuccess = () => resolve(req.result);
      req.onerror = () => reject(req.error);
    });
  }

  async push(event) {
    const db = await this.dbPromise;
    return new Promise((resolve, reject) => {
      const tx = db.transaction('events', 'readwrite');
      tx.objectStore('events').add({
        ...event,
        timestamp: Date.now(),
        retryCount: 0,
      });
      tx.oncomplete = resolve;
      tx.onerror = () => reject(tx.error);
    });
  }

  async getAll() {
    const db = await this.dbPromise;
    return new Promise((resolve) => {
      const tx = db.transaction('events', 'readonly');
      const req = tx.objectStore('events').getAll();
      req.onsuccess = () => resolve(req.result);
    });
  }

  async clearIds(ids) {
    const db = await this.dbPromise;
    const tx = db.transaction('events', 'readwrite');
    const store = tx.objectStore('events');
    ids.forEach(id => store.delete(id));
    return new Promise(resolve => tx.oncomplete = resolve);
  }
}

// ✅ 页面启动时检查上次未上报的"孤儿"记录
async function recoverOrphanEvents(queue) {
  const events = await queue.getAll();
  if (events.length === 0) return;

  console.log(`[Tracker] 发现 ${events.length} 条未上报事件,开始断点续传`);
  const ok = navigator.sendBeacon('/api/track', JSON.stringify(events));
  if (ok) {
    await queue.clearIds(events.map(e => e.id));
  }
}

第二层:多级退出通道——分层优先,不把鸡蛋放在 beacon 一个篮子里

class ExitReporter {
  constructor(queue) {
    this.queue = queue;
    this._bindExitEvents();
  }

  _bindExitEvents() {
    // visibilitychange 比 beforeunload 更早触发,移动端更可靠
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') {
        this._flush();
      }
    });

    window.addEventListener('pagehide', () => {
      this._flush();
    }, { capture: true });
  }

  async _flush() {
    const events = await this.queue.getAll();
    if (events.length === 0) return;

    const body = JSON.stringify(events);

    // 第一优先:fetch keepalive(支持更大 body,最大约 64KB)
    const sent = await this._trySendKeepAlive(body, events);
    if (sent) return;

    // 第二优先:sendBeacon(体积限制约 64KB,超出返回 false)
    const beaconOk = navigator.sendBeacon('/api/track', body);
    if (beaconOk) {
      await this.queue.clearIds(events.map(e => e.id));
      return;
    }

    // 兜底:数据留在 IndexedDB,等下次启动续传(已在第一层实现)
    console.warn('[Tracker] 所有退出通道失败,数据已持久化等待重传');
  }

  async _trySendKeepAlive(body, events) {
    try {
      await fetch('/api/track', {
        method: 'POST',
        body,
        headers: { 'Content-Type': 'application/json' },
        keepalive: true, // 页面关闭后请求继续
      });
      await this.queue.clearIds(events.map(e => e.id));
      return true;
    } catch {
      return false;
    }
  }
}

// ✅ Service Worker 后台同步(需安装 SW,是进阶方案)
// sw.js
self.addEventListener('sync', (event) => {
  if (event.tag === 'tracker-sync') {
    event.waitUntil(flushTrackerFromSW());
  }
});

async function flushTrackerFromSW() {
  // 从 SW 侧读取 IndexedDB 并上报
  const cache = await caches.open('tracker-cache');
  const events = await getEventsFromIDB(); // 同上方的 getAll 逻辑
  if (events.length === 0) return;
  await fetch('/api/track', {
    method: 'POST',
    body: JSON.stringify(events),
    headers: { 'Content-Type': 'application/json' },
  });
}

// 主页面注册一次性后台同步
async function registerSyncOnFail() {
  if (!('serviceWorker' in navigator) || !('SyncManager' in window)) return;
  const reg = await navigator.serviceWorker.ready;
  await reg.sync.register('tracker-sync');
}

第三层:退出时机与执行预算——在最后 50ms 里抢到数据

// ✅ 利用 scheduler.postTask 保证高优先级执行(Chrome 94+)
async function flushWithBudget(events) {
  if ('scheduler' in window && 'postTask' in scheduler) {
    await scheduler.postTask(() => doFlush(events), {
      priority: 'user-blocking', // 最高优先级
    });
  } else {
    // 降级为同步执行
    doFlush(events);
  }
}

function doFlush(events) {
  // 限制 50ms 内完成:超时则发原始数据片段
  const deadline = Date.now() + 45;
  const toSend = [];

  for (const e of events) {
    if (Date.now() > deadline) {
      console.warn('[Tracker] 时间预算耗尽,截断发送');
      break;
    }
    toSend.push(e);
  }

  // 就算只有部分数据也发出去
  navigator.sendBeacon('/api/track', JSON.stringify(toSend));
}

// ✅ 监控退出上报成功率
function monitorBeaconSuccess(events) {
  const ok = navigator.sendBeacon('/api/track', JSON.stringify(events));
  reportMetric({
    event: 'exit_beacon',
    success: ok,
    eventCount: events.length,
    // sendBeacon 返回 false 说明浏览器队列已满或数据超限
  });
  return ok;
}

第4篇:微前端 JS 隔离——从变量不撞名到分布式运行时

刚面了个5年前端,简历上写着主导过大型微前端项目,对 JS 隔离有深入落地经验。我开门见山:微前端架构里,你们怎么让多个子应用的 JavaScript 互不污染?

他几乎没停顿:常用三种。iframe 隔离最彻底,但通讯成本高;快照沙箱激活时存全局快照、卸载时恢复;Proxy 沙箱伪造一个干净 window,读写都代理到假对象上,qiankun 用的就是这种。

我接着问:那 Proxy 沙箱只能拦截显式属性赋值,如果某个子应用直接改 proto 或者篡改 Array.prototype.push,你怎么办?

他迟疑了一下:可以做冻结,或者在加载前用严格模式检测。我说:就算你能防住原型链污染,Shadow Realm API 能给每个子应用一个独立全局,连 Object、Promise 这种内建对象都是全新副本,彻底隔断。你为什么还要在同一个 Realm 里用 Proxy 玩猫捉老鼠?

他直接沉默了。

⚠️ 注意:Shadow Realm 目前(2025 年底)处于 TC39 Stage 3,Chrome 等浏览器尚未正式落地,生产中仍以 Proxy 沙箱 + iframe 为主流。

第一层:执行环境级隔离——Proxy 沙箱实现与 Shadow Realm 对比

// ✅ Proxy 沙箱实现(qiankun 类似思路)
class ProxySandbox {
  constructor(name) {
    this.name = name;
    this.active = false;
    this.modifiedProps = new Map();

    const rawWindow = window;
    this.proxy = new Proxy(rawWindow, {
      set: (target, prop, value) => {
        if (this.active) {
          this.modifiedProps.set(prop, value);
        }
        return true;
      },
      get: (target, prop) => {
        // 优先从沙箱自己的修改中取值
        if (this.modifiedProps.has(prop)) {
          return this.modifiedProps.get(prop);
        }
        return Reflect.get(target, prop);
      },
    });
  }

  activate() {
    this.active = true;
  }

  deactivate() {
    this.active = false;
    // 卸载时不需要还原 window,因为修改都在 modifiedProps 里
    this.modifiedProps.clear();
  }
}

// ❌ Proxy 无法拦截原型链污染
// 子应用执行:Array.prototype.push = () => 'hacked'
// 主应用的 Array.prototype.push 也被污染了

// ✅ 防御原型链污染:在子应用加载前冻结内建对象
function freezeBuiltins() {
  [Array, Object, Function, Promise, Map, Set].forEach(ctor => {
    Object.freeze(ctor.prototype);
  });
}

// ✅ Shadow Realm(目前 Stage 3,可在 polyfill 或 Node.js 22+ 中试用)
// const realm = new ShadowRealm();
// realm.evaluate(`globalThis.myVar = 'isolated'`);
// console.log(typeof myVar); // 'undefined' — 主页面不受影响

// ✅ 降级方案:同源 Worker 实现逻辑隔离
function createWorkerSandbox(scriptUrl) {
  const worker = new Worker(scriptUrl, { type: 'module' });

  // 主线程与 Worker 通过消息总线通信
  const rpc = {
    call(method, args) {
      return new Promise((resolve, reject) => {
        const id = Math.random().toString(36);
        worker.postMessage({ id, method, args });
        worker.addEventListener('message', function handler({ data }) {
          if (data.id === id) {
            worker.removeEventListener('message', handler);
            data.error ? reject(data.error) : resolve(data.result);
          }
        });
      });
    },
  };

  return rpc;
}

第二层:依赖共享与运行时去重——用 Import Maps 消灭重复 React 实例

<!-- ✅ Import Maps:全站统一 React 版本,子应用 import React 命中同一份缓存 -->
<script type="importmap">
{
  "imports": {
    "react": "https://cdn.example.com/react@18.3.0/index.js",
    "react-dom": "https://cdn.example.com/react-dom@18.3.0/index.js",
    "react-dom/client": "https://cdn.example.com/react-dom@18.3.0/client.js"
  }
}
</script>

<!-- 子应用无需打包 React,直接 import 命中全局缓存 -->
<script type="module">
  import React from 'react';    // 走 Import Map,不重复加载
  import ReactDOM from 'react-dom';
  // ...
</script>
// ✅ Module Federation(webpack 5)共享配置
// webpack.config.js(子应用)
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'subApp',
      shared: {
        react: {
          singleton: true,      // 全局只允许一个实例
          requiredVersion: '^18.0.0',
          eager: false,
        },
        'react-dom': {
          singleton: true,
          requiredVersion: '^18.0.0',
        },
      },
    }),
  ],
};

第三层:运行时可观测与故障熔断——每个子应用都有死刑线

// ✅ 用 PerformanceObserver 监控子应用长任务
function monitorSubApp(appName) {
  const observer = new PerformanceObserver((list) => {
    for (const entry of list.getEntries()) {
      if (entry.duration > 50) {
        console.warn(`[${appName}] 长任务 ${entry.duration.toFixed(0)}ms`);
        handleSubAppOverload(appName, entry.duration);
      }
    }
  });
  observer.observe({ entryTypes: ['longtask'] });

  return () => observer.disconnect(); // 返回卸载函数
}

function handleSubAppOverload(appName, duration) {
  // 上报监控
  reportMetric({ app: appName, type: 'longtask', duration });

  // 超过 3 次触发降级
  const count = (overloadCounts[appName] = (overloadCounts[appName] ?? 0) + 1);
  if (count >= 3) {
    console.error(`[${appName}] 连续超载,降级为静态快照`);
    unmountSubApp(appName);
    showFallbackSnapshot(appName);
  }
}

// ✅ 子应用崩溃隔离(Worker 方案,崩溃不影响主应用)
function createIsolatedSubApp(scriptUrl) {
  let worker = new Worker(scriptUrl);

  worker.onerror = (e) => {
    console.error('[子应用崩溃]', e.message);
    reportMetric({ type: 'subapp-crash', url: scriptUrl, error: e.message });
    // 主应用只收到通知,不崩溃
    showFallbackUI();
    // 可选:自动重启
    worker = new Worker(scriptUrl);
  };

  return worker;
}

const overloadCounts = {};

第5篇:2026 年前端求职备考路线——精准对标,高效提分

现在前端求职内卷激烈,拼的不是无效时长,而是精准对标考点的高效提分。只要心态稳,肯沉下心,闭环练题加实操,完全能跟上面试新标准。

第一阶段:基础夯实期

主攻语义化标签、H5 新特性、页面结构规范,CSS 核心(盒模型、flex/grid、动画),以及原生 JS 核心原理。

// 面试高频手写:实现 Promise.allSettled
function myAllSettled(promises) {
  return Promise.all(
    promises.map(p =>
      Promise.resolve(p).then(
        value => ({ status: 'fulfilled', value }),
        reason => ({ status: 'rejected', reason })
      )
    )
  );
}

// 面试高频手写:防抖
function debounce(fn, delay) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// 面试高频手写:原型继承
function Animal(name) {
  this.name = name;
}
Animal.prototype.speak = function () {
  return `${this.name} makes a sound.`;
};

function Dog(name) {
  Animal.call(this, name); // 继承实例属性
}
Dog.prototype = Object.create(Animal.prototype); // 继承原型方法
Dog.prototype.constructor = Dog;

// 事件循环(口述必备)
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 输出顺序:1 → 4 → 3 → 2(同步 → 微任务 → 宏任务)

第二阶段:ES6+ 语法进阶期

// Set / Map 高频考点
const set = new Set([1, 2, 2, 3]); // {1, 2, 3} 自动去重
const map = new Map([['key', 'value']]);

// 解构 + 默认值 + 重命名
const { name: userName = 'anonymous', age = 0 } = user ?? {};

// 可选链 + 空值合并(面试常问区别)
const city = user?.address?.city ?? '未知城市';
// ?. 处理 null/undefined;?? 只在 null/undefined 时取默认值,不同于 ||(0、'' 也会触发 ||)

// 模块化:export / import 静态分析 vs require 动态加载
export const util = () => {};          // 命名导出
export default class MyClass {}        // 默认导出
import { util } from './util.js';      // 静态导入,构建时 Tree Shaking
const module = await import('./heavy') // 动态导入,按需加载

// Generator + async/await 底层关系
function* gen() {
  const a = yield Promise.resolve(1);
  const b = yield Promise.resolve(2);
  return a + b;
}
// async/await 本质是 Generator + 自动执行器(Promise 驱动)

第三阶段:框架深根加原理期

// Vue 3 响应式原理(Proxy 实现)
function reactive(target) {
  return new Proxy(target, {
    get(obj, key, receiver) {
      track(obj, key); // 依赖收集
      return Reflect.get(obj, key, receiver);
    },
    set(obj, key, value, receiver) {
      const result = Reflect.set(obj, key, value, receiver);
      trigger(obj, key); // 触发更新
      return result;
    },
  });
}

// React Hooks 闭包陷阱(面试必考)
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      // ❌ 闭包捕获的 count 永远是 0
      // console.log(count);
      // setCount(count + 1);

      // ✅ 用函数式更新,始终拿到最新值
      setCount(prev => prev + 1);
    }, 1000);
    return () => clearInterval(timer);
  }, []); // 空依赖数组,effect 只运行一次

  return <div>{count}</div>;
}

// React 性能优化:memo + useCallback 避免子组件无效重渲染
const Child = React.memo(({ onClick }) => {
  console.log('Child render');
  return <button onClick={onClick}>Click</button>;
});

function Parent() {
  const [count, setCount] = useState(0);
  // ✅ useCallback 缓存函数引用,count 变化时 Child 不重渲染
  const handleClick = useCallback(() => console.log('clicked'), []);
  return (
    <>
      <span>{count}</span>
      <button onClick={() => setCount(c => c + 1)}>+</button>
      <Child onClick={handleClick} />
    </>
  );
}

第四阶段:实战项目期

// 权限控制(中后台必考)
const routes = [
  {
    path: '/admin',
    component: AdminPage,
    meta: { roles: ['admin', 'superAdmin'] },
  },
];

router.beforeEach((to, _from, next) => {
  const userRoles = store.state.user.roles;
  const required = to.meta.roles;

  if (!required || required.some(r => userRoles.includes(r))) {
    next();
  } else {
    next('/403');
  }
});

// 虚拟列表(海量数据性能优化)
function VirtualList({ items, itemHeight = 50, containerHeight = 500 }) {
  const [scrollTop, setScrollTop] = useState(0);
  const visibleCount = Math.ceil(containerHeight / itemHeight);
  const startIndex = Math.floor(scrollTop / itemHeight);
  const endIndex = Math.min(startIndex + visibleCount + 1, items.length);
  const visibleItems = items.slice(startIndex, endIndex);
  const offsetY = startIndex * itemHeight;

  return (
    <div
      style={{ height: containerHeight, overflowY: 'auto' }}
      onScroll={e => setScrollTop(e.target.scrollTop)}
    >
      <div style={{ height: items.length * itemHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, i) => (
            <div key={startIndex + i} style={{ height: itemHeight }}>
              {item.name}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

第6篇:数据驱动视图——不是自动驱动,是调度式治理

刚面了个6年前端,简历写着深入理解数据驱动视图。我直接问:用口述解释它和 jQuery 操作 DOM 的本质区别。

他答:以前手动拼 DOM 找节点,现在框架用响应式系统追踪状态,自动算最小变化,程序员只关心数据。我问:复杂表单 30 个输入框双向绑定,用户快速输入时怎么保证不掉帧?他说框架用异步批量更新,改数据合并成一次微任务。我接着问:那一次微任务算出来超 8ms,下一帧来不及怎么办?他犹豫说加 debounce。我反问:防抖 400ms 意味着每敲一键半天没反馈,这叫数据驱动视图,是你把驱动轮子拆了。

第一层:数据驱动不是自动驱动,是变更代理与调度式更新

// ✅ 高频数据(行情推送):脱离响应式,用 rAF 批量提交
class HighFreqStore {
  constructor() {
    this._raw = {}; // 裸对象,不进响应式
    this._dirty = false;
    this._subscribers = [];
  }

  set(key, value) {
    this._raw[key] = value;
    if (!this._dirty) {
      this._dirty = true;
      requestAnimationFrame(() => {
        this._subscribers.forEach(fn => fn(this._raw));
        this._dirty = false;
      });
    }
  }

  subscribe(fn) {
    this._subscribers.push(fn);
  }
}

// 股票价格每 200ms 更新,不走响应式,只在 rAF 里刷新一次 DOM
const stockStore = new HighFreqStore();
stockStore.subscribe(data => {
  document.getElementById('price').textContent = data.price;
});
setInterval(() => stockStore.set('price', Math.random() * 100), 200);

// ✅ scheduler.postTask(Chrome 94+):给中频数据分配优先级
async function updateForm(data) {
  if ('scheduler' in window) {
    await scheduler.postTask(() => {
      formState.value = data;
    }, { priority: 'user-visible' }); // 不是 user-blocking,让出给输入事件
  } else {
    // 降级:用 Promise.resolve() 放入微任务队列
    await Promise.resolve();
    formState.value = data;
  }
}

第二层:精确订阅——响应式孤岛,避免 A 的高频抖动带跑 C

// Vue 3 中用 shallowRef + watchEffect 实现孤岛隔离

import { shallowRef, watchEffect, computed } from 'vue';

// A:高频股票价(每 200ms 变)
const stockPrice = shallowRef(0);

// B:用户表单(低频)
const formData = shallowRef({ amount: 100 });

// C 的快照缓存:只有距上次超 500ms 且 C 可见时才重算
let lastSnapshotTime = 0;
const chartSnapshot = shallowRef({ price: 0, amount: 100 });

watchEffect(() => {
  const now = Date.now();
  // C 不直接订阅 stockPrice,而是定时读快照
  if (now - lastSnapshotTime > 500) {
    chartSnapshot.value = {
      price: stockPrice.value,
      amount: formData.value.amount,
    };
    lastSnapshotTime = now;
  }
});

// React 中用 useSyncExternalStore 实现类似的孤岛订阅
import { useSyncExternalStore } from 'react';

function useStockPrice() {
  return useSyncExternalStore(
    stockStore.subscribe.bind(stockStore),
    () => stockStore.snapshot(), // 快照函数
  );
}

第三层:SSR 水合接缝——防止激活前后 DOM 冲突

// ✅ React 18 选择性水合(Suspense 包裹低优先级区域)
import { Suspense, lazy } from 'react';

const HeavyChart = lazy(() => import('./HeavyChart'));

function Page({ serverData }) {
  return (
    <div>
      {/* 高优先区域:立即水合,保证可交互 */}
      <Header data={serverData.header} />
      <UserForm data={serverData.form} />

      {/* 低优先区域:延迟水合,不阻塞首屏交互 */}
      <Suspense fallback={<ChartSkeleton />}>
        <HeavyChart data={serverData.chart} />
      </Suspense>
    </div>
  );
}

// ✅ 版本号 diff:检测服务端快照与客户端最新数据的差异
async function hydrateWithVersionCheck(serverSnapshot, fetchLatest) {
  const latest = await fetchLatest();

  if (latest.version === serverSnapshot.version) {
    // 无差异,直接水合
    return hydrateDOM(serverSnapshot);
  }

  // 只有非交互区域有变化,延迟更新
  const diffKeys = getDiff(serverSnapshot, latest);
  const isInteractiveArea = diffKeys.some(k => INTERACTIVE_FIELDS.includes(k));

  if (!isInteractiveArea) {
    // 后台静默替换
    requestIdleCallback(() => hydrateDOM(latest));
  } else {
    // 交互区有变化,渐进式水合
    hydrateDOM(serverSnapshot);           // 先用服务端数据保证可交互
    requestIdleCallback(() => patchDOM(latest)); // 空闲时打补丁
  }
}

const INTERACTIVE_FIELDS = ['formData', 'userInput'];

第7篇:script 放 head 还是 body——2026 年这道题已被彻底重构

面了一个前端,简历上写精通性能优化与浏览器渲染原理。我问:JS 放在 head 里和放在 body 底部到底有什么区别?

候选人答得很快:放 head 里会阻塞页面渲染,因为浏览器遇到 script 标签就要先下载执行,执行完再继续解析 HTML,导致白屏时间长。放 body 底部就不会阻塞 DOM 树的构建,页面能尽早出来。我点点头——那你有没有想过,即使放 body 底部,如果脚本执行本身要 1 秒,用户狂点按钮,一个都没反应,这叫不阻塞渲染了吗?

第一层:传统阻塞模型的真实边界

<!-- ❌ 同步脚本:阻塞 HTML 解析 + 渲染 -->
<head>
  <script src="heavy.js"></script> <!-- 下载 + 执行完才继续解析 -->
</head>

<!-- ✅ defer:不阻塞解析,DOMContentLoaded 之前按顺序执行 -->
<head>
  <script src="app.js" defer></script>
  <script src="vendor.js" defer></script>
  <!-- 保证 vendor 先于 app 执行 -->
</head>

<!-- ✅ async:不阻塞解析,下载完立即执行(不保证顺序) -->
<head>
  <script src="analytics.js" async></script>
  <!-- 适合无依赖的第三方脚本 -->
</head>
// 核心区别口述:放底部只是"视觉上快",交互阻塞依然存在
// 用 Long Task API 验证脚本执行时长
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.warn(`长任务 ${entry.duration.toFixed(0)}ms,阻塞了交互`);
    // 即使脚本放底部,执行超 50ms 就是长任务,FID/INP 恶化
  }
});
observer.observe({ entryTypes: ['longtask'] });

第二层:async/defer 没解决的根本问题

// defer 把阻塞从"解析阶段"后移到"交互就绪阶段"
// 所有 defer 脚本执行完,页面才真正可交互

// 验证:DOMContentLoaded 触发时机
document.addEventListener('DOMContentLoaded', () => {
  // defer 脚本全部执行完毕后才触发这里
  // 如果 defer 脚本共耗时 800ms,用户在这 800ms 里交互全冻结
  console.log('DOM 就绪,defer 脚本执行完毕');
});

// ✅ 解决方案:把非关键逻辑推入 requestIdleCallback
document.addEventListener('DOMContentLoaded', () => {
  // 先激活核心交互
  initCriticalUI();

  // 非关键逻辑等浏览器空闲时执行
  requestIdleCallback(() => {
    initAnalytics();
    initChatPlugin();
  }, { timeout: 2000 }); // 最迟 2 秒后强制执行
});

第三层:2026 年的调度策略——拓扑取代物理位置

<!-- ✅ blocking="render":显式声明哪些脚本必须阻塞渲染 -->
<!-- 目前 Chrome 105+ 支持 -->
<script src="critical-polyfill.js" blocking="render"></script>

<!-- ✅ fetchpriority:控制下载优先级 -->
<link rel="preload" href="app.js" as="script" fetchpriority="high">
<link rel="preload" href="analytics.js" as="script" fetchpriority="low">

<!-- ✅ modulepreload:预加载 ES Module 及其依赖图 -->
<link rel="modulepreload" href="/src/main.js">
<link rel="modulepreload" href="/src/utils.js">
// ✅ 把计算密集型逻辑推入 Web Worker,主线程只保留渲染逻辑
// main.js
const worker = new Worker('/heavy-worker.js', { type: 'module' });
worker.postMessage({ type: 'COMPUTE', data: largeDataSet });
worker.onmessage = ({ data }) => {
  // Worker 计算完成,主线程只做渲染
  renderChart(data.result);
};

// heavy-worker.js(在 Worker 线程执行,不占主线程)
self.onmessage = ({ data }) => {
  if (data.type === 'COMPUTE') {
    const result = heavyComputation(data.data);
    self.postMessage({ result });
  }
};

// ✅ SSR 水合阶段:分片执行,不让水合成为长任务
async function hydrateInChunks(components) {
  for (const comp of components) {
    // 每次水合前检查是否还有时间预算
    await scheduler.yield?.() ?? await new Promise(r => setTimeout(r, 0));
    hydrateComponent(comp);
  }
}

第8篇:JS 阻塞渲染的真相——从解析阻塞到主线程抢占

刚面了个6年前端,写精通浏览器渲染原理与性能优化。我问他:JS 的加载会阻塞浏览器渲染吗?

他答得很快:会,遇到 script 就暂停解析,所以放底部或者用 async/defer。我追问:电商首页头部有个同步风控脚本,CDN 抖动延迟 3 秒,这 3 秒浏览器到底在干什么?阻塞的是解析还是渲染?有没有可能先画出上方骨架?

他顿住:应该不行,脚本可能操作 DOM。我接着问:那如果把 CSS 放在同步脚本之前呢?脚本下载被 CSS 阻塞(浏览器会等 CSSOM 构建完再执行脚本),渲染又被谁阻塞,预加载扫描器在这中间做了什么?

他开始乱了。

⚠️ 注意:浏览器解析到内联 script 前,若存在未完成的 CSS 加载,会先等 CSSOM 就绪再执行脚本(因为脚本可能查询样式)。这是 CSS 间接阻塞脚本执行的真正原因。

第一层:加载编排——让脚本下载时机服从视觉优先级

<!-- ✅ Early Hints(103 状态码):服务端在 HTML 返回前推送关键资源 -->
<!-- 服务器响应头 -->
<!--
HTTP/1.1 103 Early Hints
Link: </styles/critical.css>; rel=preload; as=style
Link: </scripts/app.js>; rel=preload; as=script
-->

<!-- ✅ preload + fetchpriority:关键资源最高优先级 -->
<link rel="preload" href="/scripts/risk-control.js" as="script" fetchpriority="high">

<!-- ✅ 非关键第三方脚本:低优先级 + defer -->
<script src="https://cdn.analytics.com/track.js" defer fetchpriority="low"></script>
// ✅ 把第三方脚本移入 Worker(partytown 方案思路)
// 主线程只做 DOM,analytics、chat 等推到 Worker
function loadInWorker(scriptUrl, commsConfig) {
  const worker = new Worker('/partytown-worker.js');
  worker.postMessage({ scriptUrl, commsConfig });

  // Worker 内执行第三方脚本,通过消息同步回主线程的 DOM 操作
  worker.onmessage = ({ data }) => {
    if (data.type === 'DOM_OP') {
      executeDOMOperation(data.op);
    }
  };
}

第二层:执行解耦——JS 执行对像素管线的干扰进入毫秒级可控

// ✅ 首屏脚本执行时间限制在 50ms 内(移动端更严格)
function scheduleHeavyInit(tasks) {
  // 把初始化任务拆成多个小片,放入 rIC
  function processNext(deadline) {
    while (tasks.length > 0 && deadline.timeRemaining() > 5) {
      const task = tasks.shift();
      task();
    }
    if (tasks.length > 0) {
      requestIdleCallback(processNext, { timeout: 1000 });
    }
  }
  requestIdleCallback(processNext, { timeout: 1000 });
}

// ✅ INP 优化:交互事件优先于渲染
document.getElementById('buy-btn').addEventListener('click', async (e) => {
  // 立即给用户反馈(同步,不等数据)
  e.currentTarget.disabled = true;
  showLoadingSpinner();

  // 异步处理业务逻辑,不阻塞渲染
  try {
    const result = await submitOrder();
    showSuccess(result);
  } catch (err) {
    showError(err);
    e.currentTarget.disabled = false;
  }
});

// ✅ React 18 Concurrent 模式:startTransition 标记低优先级更新
import { startTransition, useState } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  function handleInput(value) {
    setQuery(value); // 高优先级:立即更新输入框

    startTransition(() => {
      // 低优先级:搜索结果可以被新的输入打断
      setResults(searchData(value));
    });
  }

  return (
    <>
      <input value={query} onChange={e => handleInput(e.target.value)} />
      <ResultList items={results} />
    </>
  );
}

第三层:可观测熔断——精确揪出是哪个脚本吃掉了首屏

// ✅ 用 PerformanceObserver 精确定位阻塞脚本
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // 找出耗时超 100ms 的资源
    if (entry.entryType === 'resource' && entry.duration > 100) {
      const breakdown = {
        name: entry.name,
        total: entry.duration.toFixed(0),
        // 区分下载慢、解析慢、执行慢
        dns: (entry.domainLookupEnd - entry.domainLookupStart).toFixed(0),
        connect: (entry.connectEnd - entry.connectStart).toFixed(0),
        download: (entry.responseEnd - entry.responseStart).toFixed(0),
        // 注意:单纯的脚本执行时间需要配合 longtask 才能区分
      };
      console.table(breakdown);
      reportBlockingScript(breakdown);
    }
  }
});
observer.observe({ entryTypes: ['resource', 'longtask', 'navigation'] });

// ✅ 动态脚本降级:第三方脚本执行超时自动降级
function loadThirdPartyWithCircuitBreaker(src, budgetMs = 150) {
  return new Promise((resolve, reject) => {
    const start = performance.now();
    const script = document.createElement('script');
    script.src = src;
    script.async = true;
    script.defer = true;

    const timeout = setTimeout(() => {
      script.remove();
      console.warn(`[熔断] ${src} 超过 ${budgetMs}ms 未加载,已降级`);
      reportMetric({ type: 'script-timeout', src, budget: budgetMs });
      resolve(null); // 降级为 null,不阻塞主流程
    }, budgetMs);

    script.onload = () => {
      clearTimeout(timeout);
      const cost = performance.now() - start;
      reportMetric({ type: 'script-load', src, cost });
      resolve(script);
    };

    script.onerror = () => {
      clearTimeout(timeout);
      reject(new Error(`脚本加载失败: ${src}`));
    };

    document.head.appendChild(script);
  });
}

第9篇:SPA 首屏加载慢——打赢四场战役,压缩到 300ms 以内

面了一个6年前端,简历写着主导过日均千万 PV 的电商 SPA 重构性能优化。我问:SPA 首屏加载慢,你怎么解决?

他放下杯子:代码分割加路由懒加载、Tree Shaking、CDN、开 Brotli、关键资源 preload、dns-prefetch,要更快就预渲染或 SSR。我点点头——标准答案。可你说代码分割,首屏入口 chunk 里依然躺着框架运行时、状态管理、埋点脚本等无法剔除的基础设施,这个初始包体积你怎么继续往下压?

第一战役:网络与资源调度

<!-- ✅ preconnect:提前建立与关键域名的连接 -->
<link rel="preconnect" href="https://api.example.com">
<link rel="preconnect" href="https://cdn.example.com" crossorigin>

<!-- ✅ modulepreload:预加载 ES Module 及其依赖 -->
<link rel="modulepreload" href="/src/main.js">

<!-- ✅ fetchpriority:LCP 图片最高优先级 -->
<img src="/hero.webp" fetchpriority="high" loading="eager" alt="hero">

<!-- 低优先级图片 -->
<img src="/banner.webp" fetchpriority="low" loading="lazy" alt="banner">
// ✅ Service Worker:stale-while-revalidate 策略
// sw.js
const SHELL_CACHE = 'shell-v1';
const SHELL_URLS = ['/', '/index.html', '/styles/main.css', '/scripts/main.js'];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(SHELL_CACHE).then(cache => cache.addAll(SHELL_URLS))
  );
  self.skipWaiting();
});

self.addEventListener('fetch', (event) => {
  if (SHELL_URLS.includes(new URL(event.request.url).pathname)) {
    event.respondWith(
      caches.open(SHELL_CACHE).then(async (cache) => {
        const cached = await cache.match(event.request);
        // 立即返回缓存,后台静默更新
        const networkFetch = fetch(event.request).then(res => {
          cache.put(event.request, res.clone());
          return res;
        });
        return cached ?? networkFetch;
      })
    );
  }
});

// ✅ Navigation Preload:解决 SW 启动延时
self.addEventListener('activate', (event) => {
  event.waitUntil(self.registration.navigationPreload?.enable());
});

第二战役:构建与交付

// ✅ 路由级代码分割(React)
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

const Home = lazy(() => import('./pages/Home'));
const Product = lazy(() => import('./pages/Product'));
const Cart = lazy(() => import('./pages/Cart'));

function App() {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/product/:id" element={<Product />} />
        <Route path="/cart" element={<Cart />} />
      </Routes>
    </Suspense>
  );
}

// ✅ 组件级懒加载(用户交互时才加载)
function ProductPage() {
  const [showReviews, setShowReviews] = useState(false);
  const Reviews = showReviews ? lazy(() => import('./Reviews')) : null;

  return (
    <div>
      <ProductInfo />
      <button onClick={() => setShowReviews(true)}>查看评价</button>
      {Reviews && <Suspense fallback={<Spinner />}><Reviews /></Suspense>}
    </div>
  );
}

// ✅ Webpack Bundle Analyzer 找出体积大头
// package.json
// "analyze": "webpack-bundle-analyzer dist/stats.json"
<!-- ✅ Import Maps:浏览器层面共享依赖,子应用无需打包框架 -->
<script type="importmap">
{
  "imports": {
    "react": "https://cdn.skypack.dev/react@18",
    "react-dom/client": "https://cdn.skypack.dev/react-dom@18/client"
  }
}
</script>

第三战役:渲染路径

<!-- ✅ 关键 CSS 内联,消除额外 RTT -->
<style>
  /* 首屏必需的最小 CSS,直接内联 */
  .hero { width: 100%; height: 60vh; background: #f5f5f5; }
  .nav { display: flex; justify-content: space-between; }
</style>

<!-- 非关键 CSS 异步加载 -->
<link rel="stylesheet" href="/styles/full.css" media="print" onload="this.media='all'">

<!-- ✅ font-display: swap + size-adjust:防止字体阻塞文本,减少 CLS -->
<style>
  @font-face {
    font-family: 'MyFont';
    src: url('/fonts/my-font.woff2') format('woff2');
    font-display: swap;
  }
  /* 系统字体回退时用 size-adjust 匹配尺寸,减少布局偏移 */
  @font-face {
    font-family: 'MyFont-fallback';
    src: local('Arial');
    size-adjust: 105%;
    ascent-override: 90%;
  }
</style>
// ✅ content-visibility: auto:跳过视口外元素的布局和绘制
// 长列表页性能提升显著
const style = `
  .product-card {
    content-visibility: auto;
    contain-intrinsic-size: 0 300px; /* 预留高度,防止滚动条跳动 */
  }
`;

// ✅ Speculation Rules API:预渲染下一个用户可能访问的页面
// Chrome 109+
const speculationScript = document.createElement('script');
speculationScript.type = 'speculationrules';
speculationScript.textContent = JSON.stringify({
  prerender: [
    {
      where: { href_matches: '/product/*' },
      eagerness: 'moderate', // 鼠标悬浮时触发预渲染
    },
  ],
});
document.head.appendChild(speculationScript);

第四战役:持续监控与自适应降级

// ✅ 真用户监控(RUM):采集核心 Web Vitals
import { onLCP, onFCP, onTTFB, onINP, onCLS } from 'web-vitals';

function reportWebVitals(metric) {
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating, // 'good' | 'needs-improvement' | 'poor'
    navigationType: metric.navigationType,
    deviceMemory: navigator.deviceMemory,
    connection: navigator.connection?.effectiveType,
  });
  navigator.sendBeacon('/api/vitals', body);
}

onLCP(reportWebVitals);
onFCP(reportWebVitals);
onTTFB(reportWebVitals);
onINP(reportWebVitals);
onCLS(reportWebVitals);

// ✅ 自适应降级:低端设备关闭动画
function adaptToDevice() {
  const memory = navigator.deviceMemory ?? 4; // GB
  const connection = navigator.connection?.effectiveType ?? '4g';
  const cores = navigator.hardwareConcurrency ?? 4;

  if (memory < 2 || connection === '2g' || cores < 2) {
    document.documentElement.classList.add('low-end');
    // CSS: .low-end * { animation: none !important; transition: none !important; }
    disableHeavyFeatures();
  }
}

// ✅ 性能预算:CI 中用 Lighthouse 强制执行
// lighthouserc.js
module.exports = {
  ci: {
    assert: {
      assertions: {
        'first-contentful-paint': ['error', { maxNumericValue: 1500 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'total-blocking-time': ['error', { maxNumericValue: 300 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
      },
    },
  },
};

第10篇:通知用户刷新页面——从弹窗到版本一致性调度治理系统

面了一个4年前端,简历写着做过大型 B 端系统,对前端工程化和用户体验有深刻理解。我直接问:应用上线后怎么通知用户刷新页面?

他回答很快:WebSocket 推送,前端弹个 snackbar 提示用户刷新。我问:用户就是不点,连关十次怎么办?他补:加倒计时 5 秒自动刷。我继续:如果用户正在填复杂表单,你这自动刷新把人家未保存内容全毁了,怎么防御?

他开始冒汗:用 beforeunload 二次确认。我笑了:beforeunload 在现代浏览器已经被阉割得差不多了,移动端自定义文案根本不显示。况且代码触发的 location.reload() 根本拦不住 beforeunload。这不是防御,是在给自己埋生产事故。

第一层:多通道版本检测与无侵入提示

// ✅ Service Worker:安装阶段检测版本变化
// sw.js
const APP_VERSION = '__BUILD_VERSION__'; // 构建时注入

self.addEventListener('install', (event) => {
  // 新 SW 安装时,不立即激活,而是通知所有客户端
  event.waitUntil(
    self.clients.matchAll({ includeUncontrolled: true }).then(clients => {
      clients.forEach(client => {
        client.postMessage({
          type: 'NEW_VERSION_READY',
          version: APP_VERSION,
        });
      });
    })
  );
  // 不调用 skipWaiting(),等用户确认
});

// 主页面接收版本通知
navigator.serviceWorker?.addEventListener('message', ({ data }) => {
  if (data.type === 'NEW_VERSION_READY') {
    showUpdateBanner(data.version);
  }
});

// ✅ 主线程心跳轮询(5 分钟一次)
async function startVersionPolling() {
  async function check() {
    try {
      // 请求带 ETag 的版本文件,304 说明没变化
      const res = await fetch('/version.json', { cache: 'no-cache' });
      const { version } = await res.json();
      const current = window.__APP_VERSION__;
      if (version !== current) {
        handleVersionChange(version);
      }
    } catch {
      // 网络异常静默处理
    }
  }

  await check();
  setInterval(check, 5 * 60 * 1000);

  // 网络重连后立即检测
  window.addEventListener('online', check);
}

// ✅ 分级提示策略:非破坏性更新只显示静默横幅
function showUpdateBanner(newVersion) {
  // 检测用户是否正在输入
  if (navigator.scheduling?.isInputPending?.()) {
    // 推迟 300ms 后再显示
    setTimeout(() => showUpdateBanner(newVersion), 300);
    return;
  }

  const banner = document.createElement('div');
  banner.className = 'update-banner';
  banner.innerHTML = `
    <span>🎉 新版本已就绪</span>
    <button id="update-now">立即更新</button>
    <button id="update-later">稍后</button>
  `;
  document.body.prepend(banner);

  document.getElementById('update-now').onclick = () => activateNewVersion();
  document.getElementById('update-later').onclick = () => banner.remove();
}

第二层:全 tab 协同升级与 SW 无缝接管

// ✅ BroadcastChannel:通知同源所有 tab
const updateChannel = new BroadcastChannel('app-update');

// 一个 tab 决定升级,通知其他 tab
function broadcastUpdate() {
  updateChannel.postMessage({ type: 'PREPARE_UPDATE' });
}

// 其他 tab 收到通知,暂停新请求
updateChannel.onmessage = ({ data }) => {
  if (data.type === 'PREPARE_UPDATE') {
    pauseNewRequests();      // 暂停发出新的业务请求
    saveCurrentTabState();   // 暂存当前 tab 状态
  }
  if (data.type === 'UPDATE_DONE') {
    window.location.reload();
  }
};

// ✅ 表单状态暂存,刷新后恢复
function saveCurrentTabState() {
  const state = {
    formData: collectAllFormData(),
    scrollY: window.scrollY,
    timestamp: Date.now(),
  };
  sessionStorage.setItem('page-state-snapshot', JSON.stringify(state));
}

function restorePageState() {
  const snapshot = sessionStorage.getItem('page-state-snapshot');
  if (!snapshot) return;

  const { formData, scrollY, timestamp } = JSON.parse(snapshot);
  // 只恢复 30 秒内的快照
  if (Date.now() - timestamp > 30000) return;

  fillFormData(formData);
  window.scrollTo({ top: scrollY, behavior: 'instant' });
  sessionStorage.removeItem('page-state-snapshot');
}

// 页面加载时尝试恢复
document.addEventListener('DOMContentLoaded', restorePageState);

// ✅ SW 条件化 skipWaiting:等所有 tab 确认才激活
// sw.js
let pendingClients = new Set();

self.addEventListener('message', async ({ data, source }) => {
  if (data.type === 'READY_TO_UPDATE') {
    pendingClients.add(source.id);
    const allClients = await self.clients.matchAll();

    if (pendingClients.size >= allClients.length) {
      // 所有 tab 都确认了,才执行激活
      self.skipWaiting();
      const clients = await self.clients.matchAll();
      clients.forEach(c => c.postMessage({ type: 'UPDATE_DONE' }));
    }
  }
});

第三层:局部热替换——用户无感知升级

// ✅ Import Maps 动态替换:不刷新页面,按需替换模块
async function hotReplaceModule(moduleName, newUrl) {
  // 注意:Import Maps 一旦注入就无法修改,此方案需配合动态 import()
  // 实际热替换通过动态 import 新版本 URL 实现
  try {
    // 加载新版本组件
    const newModule = await import(/* webpackIgnore: true */ newUrl);

    // 替换注册表中的组件(以 Vue 为例)
    app.component(moduleName, newModule.default);
    console.log(`[热替换] ${moduleName} 已更新,用户无感知`);
  } catch (e) {
    console.error(`[热替换失败] ${moduleName},保持旧版本`, e);
  }
}

// ✅ 监控闭环:追踪版本升级关键指标
const updateMetrics = {
  bannerShown: 0,
  userAccepted: 0,
  taskAbandoned: 0, // 升级时用户正在操作导致的任务丢失

  report() {
    const acceptRate = this.userAccepted / this.bannerShown;
    reportMetric({
      event: 'version-update',
      bannerShown: this.bannerShown,
      acceptRate: acceptRate.toFixed(2),
      taskAbandoned: this.taskAbandoned,
    });

    // 接受率低于 30% 说明提示策略太激进,自动调整
    if (acceptRate < 0.3) {
      console.warn('[版本升级] 接受率过低,切换为更温和的提示策略');
      switchToGentlerStrategy();
    }
  }
};

function switchToGentlerStrategy() {
  // 降级为:只在用户主动触发关键操作时才提示
  interceptCriticalActions(() => {
    showUpdateBanner();
  });
}

function interceptCriticalActions(callback) {
  // 在支付、提交订单等高价值操作前检查版本
  document.addEventListener('click', (e) => {
    if (e.target.matches('[data-action="submit"], [data-action="pay"]')) {
      if (hasNewVersion()) callback();
    }
  });
}

这 10 道题,问的从不是某个 API 的用法,而是你在真实系统压力下,能否设计出有边界、可降级、能观测的工程解法。背答案是入门,理解每一层为什么这样设计,才是本事。