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

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

项目背景设定 面向多个国家的虚拟商品销售官网,Vue 3 + Nuxt(或自建 SSR)。核心页面是首页 / 商品列表 / 商品详情 / 下单支付要被搜索引擎收录、用户设备和网络条件差异极大、支持多语言多币种。
为什么选这三个模块 官网类项目最容易写成「切了几十个页面」。真正有含金量的是三件事:SEO 和渲染模式的选择(这是要和后端、运营一起定的架构决策)、多语言多币种的工程化(细节极多、坑极深)、首屏性能(有公认的量化指标,能测能对比)。

模块一:渲染模式与 SEO

  1. 渲染模式与 SEO(SSR / SSG 混合 + 多语言路由 + 结构化数据)★★★
    简历这样写 官网渲染架构与 SEO 改造(Vue 3 + Nuxt SSR/SSG + 多语言路由 + 结构化数据):按页面特性分配渲染模式——营销页预渲染(SSG)、商品详情服务端渲染(SSR)并按 TTL 缓存 HTML、用户中心走客户端渲染;多语言用子路径路由 + hreflang 互指,商品页注入结构化数据。首屏 LCP 由约 3.3s 降至 1.4s 上下(4G 节流条件),商品页可被搜索引擎正常抓取到完整内容(改造前抓到的是空壳)。
    展开完整拆解
    为什么要这么设计

    原来是纯客户端渲染的单页应用。功能上没问题,但两个致命缺陷。

    一是搜索引擎抓不到内容。爬虫拿到的 HTML 是一个空的 div id="app",商品名、价格、描述全靠 JS 渲染。虽然主流搜索引擎号称能执行 JS,但实际抓取的时机和完整度都不可控,收录效果很差。对一个靠自然流量获客的官网来说这是直接影响收入的问题。

    二是首屏太慢。用户要等 JS 下载、解析、执行、发请求、拿数据、渲染,才能看到内容。在网络较差的地区这个链条要好几秒,期间是白屏。

    但也不能无脑全上 SSR——SSR 有服务器成本、有 Node 服务的运维复杂度,而且有些页面压根不需要(用户中心是登录后才看的,不需要被收录,也不在乎首屏那几百毫秒)。所以核心决策是按页面分配渲染模式,而不是全站统一。

    整体链路
    页面分类与渲染模式 │ ├─ 营销页 / 帮助中心 / 落地页 → SSG(构建时预渲染成静态 HTML) │ 内容变更频率低,CDN 直接吐 HTML,最快也最省 │ ├─ 商品列表 / 商品详情 → SSR + HTML 缓存 │ 需要收录、内容会变、有个性化元素(币种/库存) │ 服务端渲染后按 TTL 缓存整页 HTML,个性化部分客户端二次填充 │ └─ 用户中心 / 订单 / 结算 → CSR(纯客户端渲染) 登录后可见、不需要收录、不希望被缓存 SSR 请求链路 浏览器/爬虫 ──▶ CDN ──▶ Node SSR 服务 ──▶ 业务 API │ │ │ ├─ 服务端取数(超时 → 降级为骨架 + 客户端补取) │ ├─ 渲染 HTML + 内联关键 CSS │ ├─ 注入 SEO 标签(title/description/canonical/hreflang) │ ├─ 注入结构化数据(商品信息) │ └─ 序列化状态到 HTML(避免客户端重复请求) │ └─ 缓存整页 HTML(按 URL + 语言 + 币种分键,TTL 数分钟) 多语言路由:/en/product/123 /ja/product/123 /es/product/123 各语言版本互相 hreflang 指向 + 自指 canonical 指向自己的语言版本,不要都指向英文版
    分步拆解
    1. 先给页面分类,再选渲染模式。判断标准三个问题:需不需要被搜索引擎收录内容变化频率有没有强个性化。不需要收录的直接 CSR;需要收录且内容基本不变的 SSG;需要收录但内容会变的 SSR。这个分类过程本身就是最值得讲的部分,比说「我用了 Nuxt」有价值得多。
    2. SSR 页面必须缓存整页 HTML。不缓存的话每个请求都要服务端渲染一次,Node 进程的 CPU 会成为瓶颈,成本和延迟都不可接受。缓存键要包含URL、语言、币种这些影响渲染结果的维度。
    3. 个性化内容不能进 SSR 的 HTML。比如「你的购物车有 3 件商品」,如果渲染进 HTML 就没法缓存了(每个用户不一样)。做法是HTML 里渲染通用内容和占位,个性化部分客户端挂载后再请求填充。这是「缓存 + 个性化」共存的标准解法。
    4. 服务端取数要有超时和降级。SSR 时如果 API 慢,整个页面都出不来,比 CSR 还糟(CSR 至少能先出骨架)。所以服务端取数设短超时,超时就渲染骨架 HTML 让客户端接管。宁可退化成 CSR,也不能让页面超时。
    5. 状态要序列化到 HTML。服务端已经取到的数据序列化进 HTML,客户端 hydrate 时直接用,不要再发一次相同的请求。漏了这一步会出现「服务端渲染了一次、客户端又请求一次」,白白多一次往返,SSR 的收益被抵消掉一半。
    6. 多语言用子路径而不是参数或者子域名。/en/ /ja/ 这种形式对 SEO 最友好(路径清晰、易于配置 hreflang),子域名要各自建立权重,查询参数容易被判定为重复内容。
    7. hreflang 必须互指且包含自指。每个语言版本的页面都要列出所有语言版本的链接,包括指向自己的那一条。漏了自指是最常见的错误。另外要有 x-default 指向语言选择页或默认版本。
    8. canonical 指向自己的语言版本。常见错误是所有语言版本的 canonical 都指向英文版,那等于告诉搜索引擎「其他语言版本都是英文版的副本,不用收录」,直接把多语言 SEO 做废了。
    9. 商品页注入结构化数据。用 JSON-LD 描述商品名、价格、币种、可用性、评分。这决定了搜索结果里能不能展示富摘要(价格、星级),对点击率影响明显。结构化数据里的价格必须和页面显示的一致,不一致会被判定为欺骗性内容。
    关键决策与取舍

    为什么不全站 SSG。SSG 最快最省,但商品价格、库存、活动信息经常变,全站 SSG 意味着每次变更都要重新构建部署,几千个商品页的构建时间会长到不可接受,而且变更生效延迟太大。折中方案是「SSR + 短 TTL 缓存」——它在效果上接近 SSG(大部分请求命中缓存直接吐 HTML),但内容变更几分钟内就能生效。这个判断的依据是内容变更频率,要能说出来。

    SSR 带来的运维成本。从「一堆静态文件丢 CDN」变成「要维护一个 Node 服务集群」,多了进程管理、内存泄漏排查、扩容、灰度这些问题。SSR 的内存泄漏尤其麻烦——服务端渲染时如果在模块作用域挂了状态(比如全局的 store 单例),会导致请求之间数据串台,而且这类 bug 在开发环境压根不复现。这个代价必须主动承认。

    踩过的坑:hydration 不匹配。服务端渲染的 HTML 和客户端首次渲染的结果不一致,控制台报警告,页面会闪一下或者事件绑不上。原因通常是渲染时用了服务端和客户端不一致的值——最典型的是 new Date()(服务端和客户端时区/时刻不同)、随机数、window 相关的判断(服务端没有 window,条件分支走的路不一样)。修法是这类逻辑挪到 onMounted 之后执行,或者用专门的客户端渲染包装。这个坑是 SSR 的头号常见问题,必须能讲。

    没做的部分:没做 ISR(增量静态再生成)。理想状态是静态页面在后台按需重新生成,兼具 SSG 的速度和 SSR 的新鲜度。当时的技术栈支持不完善,用 SSR 加缓存替代了,效果接近但服务器成本更高。

    数字是怎么测的

    LCP:用 Lighthouse 在固定的节流条件下测(比如模拟 4G、CPU 降速 4 倍),跑多次取中位数。节流条件必须说出来——不节流的话本地测 LCP 都在几百毫秒,那个数字没有参考价值。改造前约 3.3s,改造后 1.4s 上下。更有说服力的是真实用户数据(RUM),能按国家分别看 P75,但那需要埋点和数据平台支持。

    「爬虫能抓到完整内容」怎么验证:三个手段。一是用 curl 直接取 HTML 看里面有没有商品名和价格(最直接,改造前是空壳);二是用搜索引擎的站长工具做「实时抓取测试」,看它渲染出的页面;三是过一段时间用 site: 查收录量的变化。不要报「SEO 流量提升 X%」——那受内容、外链、竞品、算法更新影响太多,把它算成自己的技术成果一问就穿。

    缓存命中率:SSR 的 HTML 缓存命中率要能报出来,它直接决定服务器成本。这个数字从 CDN 或者网关的监控取。命中率如果很低,说明缓存键设计有问题(比如把用户相关的维度加进了缓存键)。

    面试追问
    Q:SSR 和 CSR 的首屏差异,具体差在哪几步? A:CSR 的链路是:请求 HTML(空壳)→ 下载 JS → 解析执行 JS → 发起数据请求 → 拿到数据 → 渲染。用户在最后一步之前一直看白屏。SSR 是:请求 HTML(服务端已经取数并渲染好)→ 浏览器直接绘制。关键差异是「数据请求的往返」从客户端挪到了服务端,而服务端到 API 的网络延迟远小于用户到 API 的延迟(同机房内网),所以这一步快得多。另外 SSR 的 HTML 里已有内容,浏览器可以边下载边绘制,不用等 JS。但 SSR 的可交互时间(TTI)不一定更好——JS 还是要下载和 hydrate,如果 JS 很大,用户会遇到「看到了但点不了」的状态,这是 SSR 的固有问题。
    Q:SSR 页面怎么做缓存又保证价格是最新的? A:分层处理。整页 HTML 缓存的 TTL 设短(几分钟),保证变更能较快生效;价格变更时主动清理该商品页的缓存(发一个失效通知给 CDN 或者网关);对强实时的部分不进 HTML——比如秒杀的库存数量,用客户端请求实时拿。这里有个必须做的一致性保护:结构化数据和页面显示的价格必须来自同一次渲染,不能一个来自缓存 HTML、一个来自客户端实时请求,否则会出现两个不同的价格,既影响 SEO 判定也影响用户信任。
    Q:用户第一次进来,怎么决定给他哪个语言? A:优先级是:URL 里的语言(最高,用户明确指定或者从搜索结果进来的)→ 用户之前的选择(cookie)→ 浏览器的 Accept-Language → IP 地理位置 → 默认语言。有几个坑:不要用 IP 强制重定向,会导致爬虫(IP 在美国)永远只能抓到英文版,其他语言版本收录不了;也会让用户无法访问自己想看的语言版本。正确做法是用推荐横幅而不是强制跳转——「检测到你在日本,是否切换到日文版」,让用户自己选。另外语言选择要能通过 URL 分享,这是子路径路由的另一个好处。
    Q:SSR 服务挂了怎么办?整个官网就打不开了吗? A:要有降级。第一层是 CDN 缓存兜底——SSR 服务挂了但 CDN 里还有缓存的 HTML,配置「源站故障时继续吐过期缓存」(stale-while-error),大部分页面还能访问。第二层是降级到 CSR——网关检测到 SSR 服务不可用时,直接返回一个静态的 CSR 空壳 HTML,页面还能用(只是首屏慢、SEO 那次抓取拿不到内容)。第三层是静态兜底页。这个降级链路要演练过才敢说,而且要注意降级期间不要让爬虫抓到空壳,可以通过 UA 判断给爬虫返回 503 让它稍后重试,而不是给它一个没内容的页面(后者会影响收录质量)。

模块二:多语言与多币种

  1. 多语言与多币种(按需加载语言包 + 本地化格式 + 时区与 RTL)★★★
    简历这样写 国际化工程体系(Vue 3 + vue-i18n + Intl API + 构建期语言包拆分):语言包按路由维度拆分并按需加载,不再一次性打进主包;日期、数字、金额统一走 Intl API 按 locale 格式化,价格以最小货币单位的整数传输、前端只负责展示;支持 RTL 布局与时区转换,并建立文案缺失的构建期检查。首包 JS 中语言包体积由约 190KB 降至按需的十几 KB,上线后未再出现金额精度与时区偏差类问题。
    展开完整拆解
    为什么要这么设计

    最早的国际化是「把所有语言的文案打进一个大 JSON,全量加载」。支持 3 种语言时还行,加到 8 种之后语言包接近 200KB,而用户只需要其中一种,剩下的全是浪费。

    更严重的是一堆隐蔽的正确性问题,这些比体积问题难缠得多:

    金额精度。后端返回浮点数的价格,前端做运算(算总价、折扣),浮点误差累积后出现「三件 9.99 显示成 29.969999」这种情况。日期时区。后端返回 UTC 时间,前端直接格式化显示,用户看到的是 UTC 时间不是本地时间,「订单在昨天创建」显示成前天。数字和日期格式。硬编码成 YYYY-MM-DD 和逗号千分位,但不同地区的习惯完全不同(有的用点做小数点、逗号做千分位)。复数和语序。把「你有 {n} 件商品」拆成「你有」+ n +「件商品」拼接,换成其他语言语序完全错乱。

    所以这个模块的核心不是「翻译」,而是把所有和地区相关的逻辑统一收口,不允许散落在业务代码里手写

    整体链路
    语言包组织(按路由拆分) locales/ en/ common.json product.json checkout.json ja/ common.json product.json checkout.json ... │ ├─ common 随主包加载(导航、按钮、错误提示) └─ 页面级语言包在路由守卫里按需 import(动态导入 → 独立 chunk) 格式化统一收口(禁止业务代码手写) │ ├─ 金额:后端传「最小货币单位整数 + 币种码」 │ { amount: 999, currency: "USD" } → 表示 9.99 美元 │ 前端用 Intl.NumberFormat(locale, {style:'currency'}) 展示 │ 前端不做金额运算,总价/折扣一律后端算 │ ├─ 日期:后端传 UTC 的 ISO 字符串 │ 前端用 Intl.DateTimeFormat(locale, {timeZone}) 转本地 │ 时区来源:用户设置 > 浏览器时区 │ ├─ 数字/百分比:Intl.NumberFormat │ └─ 复数/插值:i18n 的复数规则,不手动拼接字符串 "items": "{n} item | {n} items" ← 由 i18n 按 locale 选形式 RTL 支持 ├─ html[dir="rtl"],样式用逻辑属性(margin-inline-start 等) └─ 图标方向、进度条方向需要单独处理 构建期检查 ├─ 扫描代码中的 t('key') → 校验各语言包是否都有该 key └─ 缺失则构建失败(或告警),避免线上出现原始 key
    分步拆解
    1. 语言包按路由拆分。只有导航、按钮、通用错误提示这些随主包加载;商品页、结算页的文案在进入路由时动态导入。构建工具会自动拆成独立 chunk。要处理加载中的状态——语言包还没加载完时页面可能短暂显示原始 key,做法是在路由守卫里 await 加载完成再进入。
    2. 金额一律用最小货币单位的整数传输。9.99 美元传 999(美分),不传 9.99。这从根本上消灭了浮点精度问题。注意不同币种的小数位数不同(日元没有小数位,有些币种是 3 位),换算时要按币种的规则,不能一律除以 100。
    3. 前端不做金额运算。总价、折扣、税费全部后端算好返回。前端只负责展示。这是个纪律问题不是技术问题——只要允许前端算,就一定会有人在某个角落写 price * quantity,然后出精度问题。
    4. 格式化全部走 Intl API。浏览器原生支持,按 locale 自动处理千分位符号、小数点符号、货币符号位置、日期顺序。不要自己写格式化函数,也不要为了这个引入大的日期库(体积成本高且规则不如原生全)。
    5. 时区必须显式指定。Intl.DateTimeFormat 要传 timeZone,优先用用户在设置里选的,没有则用 Intl.DateTimeFormat().resolvedOptions().timeZone 拿浏览器时区。不要用固定偏移量(比如 +8)代替时区,因为夏令时会导致偏移量在一年中变化,用固定值必然在切换日期前后出错。
    6. 复数交给 i18n 处理。不同语言的复数规则差异很大(英语两种形式、有的语言三四种、中文日文没有复数变化)。绝对不要用字符串拼接,要用 i18n 的复数语法,让它按 locale 选择正确的形式。
    7. RTL 用 CSS 逻辑属性。margin-inline-start 代替 margin-leftpadding-inline-end 代替 padding-right。这样切换 dir="rtl" 时布局自动镜像,不需要写两套样式。但有些元素不能镜像——比如带方向含义的图标(返回箭头要镜像,播放按钮不要)、数字和代码(永远从左到右),这些要单独标记。
    8. 构建期检查文案缺失。写脚本扫描代码里所有的翻译 key,对比各语言包,缺失的报错。这是防止「线上页面显示 product.detail.title 这种原始 key」的唯一可靠手段——靠人工检查一定会漏。
    关键决策与取舍

    为什么不把翻译放到后端接口返回。那样前端不用维护语言包,但代价是每次请求都要传大量文案,而且静态文案(按钮、标签)压根不该走接口。我们的划分是:UI 文案在前端语言包,业务数据的文案(商品名、描述、运营配置的文字)由后端按 locale 返回。这个划分的依据是「变更频率和管理者」——UI 文案跟着代码走由开发管,业务文案跟着数据走由运营管。

    按需加载语言包的代价。首包小了,但切换语言时要重新加载对应的语言包,有短暂的加载态。而且如果拆分粒度太细,会导致请求数变多,在高延迟网络下反而更慢。所以粒度控制在「按路由分组」而不是「按组件分」——太细了得不偿失。

    踩过的坑:SSR 环境下 Intl 的默认时区不对。服务端渲染时 Node 进程的时区是服务器时区(通常 UTC),渲染出的日期是 UTC 时间;客户端 hydrate 时用的是用户本地时区,两边不一致,导致 hydration 警告和日期闪烁。修法是日期这类依赖环境的内容不在 SSR 阶段渲染,服务端输出占位或者 UTC 的原始值,客户端挂载后格式化。或者把用户时区通过 cookie 传给服务端,两边用同一个时区渲染。这个坑是 SSR 和国际化叠加才会出现的,能讲出来说明是真做过。

    没做的部分:没做翻译平台对接。文案是靠人工维护 JSON 文件,翻译更新要提代码。规模再大应该接翻译管理平台,让翻译人员直接在平台上改,前端通过 API 或者构建时拉取。

    数字是怎么测的

    语言包体积:看构建产物的分析报告(webpack-bundle-analyzer 之类),对比改造前后主包里语言包相关的体积。改造前约 190KB(8 种语言全打进去),改造后主包只有 common 部分十几 KB。要说清是压缩前还是压缩后的体积——gzip 后 JSON 的压缩率很高,两个数字能差三四倍,不说明白容易被认为在夸大。

    「未再出现金额精度与时区偏差问题」:这是定性描述,依据是问题工单的分类统计。不要写成「精度问题降为 0」——绝对化的表述一定会被追问,而且只要有一个漏网的案例就被推翻。用「未再出现」并且能说出改造前的具体案例(比如「三件 9.99 显示成 29.969999」),比给一个百分比可信得多。

    可以补充的验证手段:写单元测试覆盖各 locale 下的格式化输出(美元/日元/欧元的显示、夏令时切换日的日期转换)。这类正确性改造用测试用例数量比用百分比更有说服力。

    面试追问
    Q:为什么金额要用整数传?后端传字符串不行吗? A:字符串也可以,而且比浮点数好——它不会有精度丢失。但整数更好用,因为整数可以直接参与比较和排序,字符串要先转换;而且整数明确表达了「这是一个精确值,不是近似值」的语义。真正不能用的是浮点数,因为 JS 的 Number 是双精度浮点,0.1 加 0.2 不等于 0.3,金额一旦经过运算就可能出偏差。如果后端非要传小数,前端应该用字符串接收并用 Decimal 类库处理,绝不能直接转成 Number 做运算。
    Q:用户在不同国家看到的价格不一样,这是前端换算的吗? A:绝对不能前端换算。汇率是业务数据,涉及取整规则、汇率时效、定价策略(很多时候不是按汇率直接换算,而是有本地化定价,比如日本区定 1000 日元这种整数价)。前端换算会导致:汇率不一致(前端缓存的汇率过期)、和下单时后端计算的金额不符(用户看到一个价、扣款另一个价,这是资损和投诉的根源)。正确做法是后端按用户所在市场返回该市场的定价,前端只负责按 locale 格式化展示。
    Q:RTL 布局改造,除了镜像样式还有什么坑? A:几个容易漏的。一是图标和插画——箭头、返回按钮、进度指示要镜像,但品牌 logo、播放按钮、有文字的图片不能镜像。做法是给需要镜像的图标加统一的 class,在 RTL 下 transform: scaleX(-1)二是混排——阿拉伯语句子里嵌了英文品牌名或数字,方向会混乱,需要用 Unicode 的方向控制字符或者 bdi 标签隔离。三是动画方向,轮播、滑入这些动效在 RTL 下应该反向。四是第三方组件,很多组件库的 RTL 支持不完整,日期选择器、下拉菜单的定位经常出问题,要逐个验证。RTL 的工作量通常被严重低估,真正的成本在这些细节上。
    Q:文案里需要插入链接或者加粗,怎么处理? A:不能用字符串拼接 HTML(有 XSS 风险,而且语序会错)。正确做法是用 i18n 的组件插值——文案里写占位标记,渲染时把标记替换成 Vue 组件或者插槽内容。比如「阅读并同意 {terms}」,terms 插槽里放一个 router-link。这样翻译人员可以自由调整占位的位置以适应各语言的语序,而链接本身由代码控制。如果确实要渲染富文本文案(比如运营配置的带格式的公告),必须先做 HTML 净化,只允许白名单标签。

模块三:首屏性能与前端监控

  1. 首屏性能与前端监控(体积治理 + 关键资源优先 + 错误与性能上报)★★★
    简历这样写 首屏性能治理与前端监控体系(Vue 3 + Vite 构建分析 + 图片自适应 + Web Vitals + 自建上报):通过路由级代码分割、依赖替换与体积门禁治理产物,图片按设备响应式尺寸与现代格式下发并对首屏图预加载;建立Web Vitals 与 JS 错误上报,按国家与机型分维度看分布。首包 JS(gzip 后)由约 310KB 降至 130KB 上下,LCP 元素由「等 JS 执行后才出现」改为 HTML 内直接可见,线上 JS 错误可定位到具体版本与用户环境。
    展开完整拆解
    为什么要这么设计

    性能问题的起点是没有数据。改造前团队对性能的认知只有「我这边打开挺快的」——因为开发都在网络好的地方用好机器。而真实用户分布在多个国家,一部分人用几年前的低端安卓机、3G 或者不稳定的 4G。没有分国家、分机型的监控数据,压根不知道该优化什么。

    建了监控之后问题清晰了:首包 JS 三百多 KB(gzip 后),在慢网络下要几秒才下载完;LCP 元素是首页的主视觉图,而它是 JS 执行后才插入的,等于必须等完整个 JS 链路;一部分低端机上 hydrate 的 CPU 耗时明显。

    所以三个方向:把首包压下去让 LCP 元素尽早出现持续监控防止回退。第三点最容易被忽略但最重要——性能优化做完不加门禁,三个月后就会退回原样,因为每个人都只多加一点点依赖。

    整体链路
    体积治理 │ ├─ 构建产物分析 → 找出体积大头 │ ├─ 路由级代码分割(每个页面独立 chunk) │ ├─ 大依赖替换(日期库 → 原生 Intl;工具库 → 按需引入) │ ├─ 重复依赖合并(同一个库被打进多个 chunk 的多个版本) │ └─ polyfill 按目标浏览器裁剪(不给现代浏览器发旧 polyfill) │ └─ 体积门禁:CI 中检查产物体积,超过阈值则构建失败 关键资源优先 │ ├─ 关键 CSS 内联到 HTML(避免 CSS 阻塞首次绘制) ├─ 首屏图 <link rel="preload"> + fetchpriority="high" ├─ 非关键 JS 用 defer / 动态导入 ├─ 字体:font-display: swap + 只加载用到的字重 └─ 图片:srcset 按设备宽度 + 现代格式(AVIF/WebP)+ 非首屏懒加载 必须写明 width/height(避免布局偏移 CLS) 监控上报 │ ├─ 性能:Web Vitals(LCP / CLS / INP)→ 上报 ├─ 错误:window.onerror + unhandledrejection + Vue errorHandler │ → 带上 sourcemap 可还原的版本号 ├─ 维度:国家 / 网络类型 / 设备档次 / 应用版本 / 页面 └─ 采样:性能全量或高比例采样;错误全量(错误量本身不大) 看板:按国家看 P75 的 LCP,找出最差的市场针对性优化
    分步拆解
    1. 先分析再优化。用构建分析工具看产物构成,找出体积占比最大的几个依赖。不要凭感觉优化——实际测下来占比最大的常常是意料之外的东西(一个只用了一个函数的大工具库、一个被重复打包的 polyfill、一份忘了删的图标字体)。
    2. 路由级代码分割是收益最大的一步。每个页面单独一个 chunk,用户进首页只下载首页的代码。要注意共享依赖的提取——如果多个 chunk 都用到某个库,要提到公共 chunk 里避免重复,但也不能把所有共享的都提到主包(那又变大了)。
    3. 大依赖能换就换。日期格式化用原生 Intl 替代日期库;工具函数改成按需引入或者手写几个;图表库如果只用一种图,换成体积小的专用库。每个替换都要评估「省下的体积」对「重写的成本和风险」,不是所有依赖都值得换。
    4. LCP 元素必须在 HTML 里。首页的主视觉图如果是 JS 渲染出来的,LCP 就一定要等 JS。改成 SSR 输出(或者直接写在 HTML 模板里),配合 preloadfetchpriority="high",让浏览器一拿到 HTML 就开始下载这张图。这一步对 LCP 的改善通常比压缩 JS 体积更直接。
    5. 图片按设备下发。srcsetsizes 让浏览器按屏幕宽度和 DPR 选合适的尺寸,用 picture 提供现代格式并保留兜底。给手机发一张 2000px 宽的图是最常见也最浪费的错误。
    6. 图片必须写宽高。不写的话图片加载前占 0 高度,加载后把下面内容挤下去,产生布局偏移(CLS)。写死宽高或者用 aspect-ratio 预留空间。CLS 是三个核心指标里最容易修也最容易被忽略的。
    7. 字体要控制。font-display: swap 避免字体加载期间文字不可见;只加载实际用到的字重(很多项目引了 6 个字重实际只用 2 个);中日韩字体要做子集化,全量字体文件几兆,必须按实际用到的字符裁剪。
    8. 监控要带足维度。只有一个全局的 LCP 平均值没用。要能按国家、网络类型、设备档次、页面下钻,才能定位到「某个国家的某类机型上某个页面特别慢」。取 P75 而不是平均值,这也是 Web Vitals 的官方推荐。
    9. 错误上报要能还原堆栈。生产环境代码是压缩过的,报错堆栈是乱码。要上传 sourcemap 到监控平台(不部署到公网),上报时带版本号,平台据此还原。没有 sourcemap 的错误监控基本没有排查价值。
    10. 加体积门禁防回退。CI 里检查产物体积,超过阈值就构建失败,强制开发者解释为什么变大。这是唯一能长期守住性能的手段,靠人的自觉一定会退化。
    关键决策与取舍

    体积门禁的阈值怎么定,以及它的副作用。阈值太松没意义,太紧会挡住正常的功能开发(有时候确实需要引入一个库)。做法是设一个当前值加少量余量的阈值,并允许通过 PR 显式提升阈值(但要走评审)。这样变大是可以的,但必须是有意识的决定而不是悄悄发生的。副作用是会引起团队摩擦,所以引入时要先沟通清楚目的,否则会被当成阻碍。

    监控上报本身的成本。上报请求会占用带宽和后端资源。做法是合并上报(攒一批一起发)、sendBeacon(不阻塞页面卸载)、性能数据采样(不需要全量,采样足够看分布)。错误数据全量上报但要做去重和限流——一个死循环里的报错能一秒上报几千次,必须在客户端就限流,否则会把监控服务打挂。

    踩过的坑:优化了指标但体验没变好。为了压 LCP,把首屏图换成了一张很小的模糊占位图,LCP 数字确实降了,但用户看到的是一张糊图,体验更差。这是典型的「优化指标而不是优化体验」。修正后的做法是保证 LCP 图片本身质量合格,通过预加载和格式优化来提速,而不是靠降质量刷指标。这个反思很值得讲——它说明你理解指标是手段不是目的。

    没做的部分:没做首屏的服务端流式渲染(把 HTML 分块流式输出,让浏览器更早开始渲染)。这对首屏有进一步的收益,但改造成本和风险较高,当时的收益优先级排在后面。

    数字是怎么测的

    首包 JS 体积:构建产物分析工具直接给出,要明确说是 gzip 后的体积(或者 brotli),因为原始体积和压缩后能差三倍以上。「310KB 降到 130KB」如果不说压缩状态,容易被认为在挑好看的数字。另外要说清「首包」包含什么——是入口 chunk 加公共 chunk,还是包含了首屏路由的 chunk。

    LCP:两套数据都要有。实验室数据用 Lighthouse 在固定节流条件下多次取中位数,用于开发时对比;真实用户数据用 Web Vitals 上报后按国家看 P75,用于判断真实效果。两者会有明显差异,实验室数据通常好看得多,所以简历上如果只报实验室数据要注明条件。

    「LCP 元素在 HTML 内直接可见」怎么验证:curl 取 HTML 看首屏图的 img 标签在不在里面;或者在 Performance 面板看 LCP 元素的出现时机是否早于 JS 执行完成。这是个结构性的改变,比数字更能说明问题。

    不要报「跳出率下降」「转化率提升」。这些业务指标受太多因素影响,把它们算成前端性能优化的成果是站不住的。如果确实有 A/B 实验数据支撑可以提,但要说清实验设计。

    面试追问
    Q:LCP、CLS、INP 分别是什么,各自怎么优化? A:LCP 是最大内容元素的渲染时间,衡量「用户多久看到主要内容」。优化方向是让 LCP 元素尽早开始下载并渲染——放进 HTML、预加载、用现代格式压小、别让它依赖 JS。CLS 是累积布局偏移,衡量「页面有没有跳来跳去」。优化方向是给图片和广告位预留空间、不在已有内容上方动态插入元素、字体切换时避免尺寸突变。INP 是交互到下一次绘制的延迟,衡量「点了多久有反应」,它替代了旧的 FID。优化方向是减少主线程长任务——拆分长任务、把重计算挪到 Web Worker、减少 hydrate 的同步工作量。这三个指标对应三种不同的体验问题,优化手段基本不重叠,这个区分是回答的关键。
    Q:你说首包从 310KB 降到 130KB,具体是靠什么降下来的?占比最大的是哪一项? A:这个问题必须能答出具体项和量级,答不出就说明是编的。真实回答应该是类似:路由级分割贡献最大(把非首屏页面的代码移出主包,几十 KB);替换日期库(那个库本身几十 KB,换成原生 Intl 后归零);语言包按需加载(前面模块讲的,几十 KB);polyfill 裁剪(按目标浏览器生成,省十几 KB);删掉未使用的图标字体和废弃组件(几 KB 到十几 KB)。要能说出「哪一项贡献最大」,因为面试官想知道你是分析出来的还是听说的。
    Q:低端机上 hydrate 慢,怎么优化? A:几个方向。减少需要 hydrate 的组件数量——静态内容(页脚、说明文字)不需要交互,可以标记为静态跳过 hydrate。延迟非首屏组件的 hydrate,等元素进入视口或者用户交互时再激活(渐进式 hydration)。把长任务拆开,让主线程有机会响应用户输入。更彻底的方案是孤岛架构——只有真正需要交互的部分才发送 JS,其余是纯静态 HTML,但那需要框架层面的支持,改造成本大。还有个容易忽略的点:hydrate 慢的一部分原因是 JS 解析本身,所以减小体积对 hydrate 也有帮助,两者不是独立的。
    Q:怎么保证优化不被后续开发退回去? A:靠自动化门禁而不是靠人。三层:CI 里的体积检查,超阈值构建失败;CI 里跑 Lighthouse,核心指标退化超过阈值则告警或阻塞;线上监控的持续跟踪,按版本看指标趋势,某个版本发布后指标恶化能立刻定位。另外要有归属——性能指标要有明确的负责人和定期回顾,没人看的看板等于不存在。这个问题问的是工程习惯而不是技术,答「我们加了 CI 门禁」比答一堆优化技巧更对题。

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

项目拆解 · 国际站官网(Vue)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据