判断依据只有两条:要不要 SEO、内容多久变一次。不要一上来就说 SSR 更好,选型的关键是把站点按页面类型拆开看,同一个官网的不同页面完全可以用不同方案。
纯 SPA 适合登录后的页面:用户中心、订单列表、后台管理。这些页面不需要 SEO(爬虫也进不来),而且数据高度个性化,服务端渲染没有意义还浪费算力。
SSG(静态生成)适合内容不常变的页面:首页、活动落地页、帮助中心、博客、商品分类页。构建时生成静态 HTML,直接扔 CDN,性能和成本都是最优的,没有服务端运行时也就没有服务端故障风险。国际业务里这一点特别有价值——静态文件走 CDN 就近分发,各地区访问都快,不需要多地区部署 Node 服务。
SSR 适合既要 SEO 又高度动态的页面:商品详情页(价格、库存、多币种都在变)、搜索结果页、短视频分享落地页。这类页面内容随时变,预生成不现实,但又必须让爬虫看到内容。
还有一个常被忽略的选项:ISR / 按需增量生成——静态生成但可以按需重新生成某个页面。商品详情这类「变但不是每秒都变」的内容用它最划算,兼顾了 CDN 性能和内容新鲜度。
我的实际方案会是混合:营销和内容页 SSG、商品详情和分享页 SSR(或 ISR)、用户中心 SPA。用 Nuxt 这类框架可以在一个项目里按路由指定渲染模式,不用拆成多个工程。
SSR 的代价必须说清楚,这是判断力的体现:需要维护 Node 服务(运维成本、扩容、监控)、服务端有 CPU 压力(渲染是计算密集的,要配合缓存)、代码要兼容两端运行(不能随便用 window,第三方库可能不支持 SSR)、调试更复杂、水合失败会导致页面「看得见但点不动」。所以不需要 SEO 的页面上 SSR 是纯粹的自找麻烦,这个判断比会用 SSR 更重要。
URL 结构是第一个决定,而且很难后改,因为改了会丢掉已有的搜索排名。三种方案:
子路径(example.com/en/、example.com/de/):权重集中在一个域名上,部署和证书最简单,是大多数情况下的最优选择。子域名(de.example.com):便于按地区独立部署和运维,但权重相对分散。独立国家域名(example.de):本地信任度最高、SEO 地域信号最强,但成本高、权重完全分散,通常只有重点市场才做。
语言和地区要分开,这是国际化最容易做错的地方:语言决定文案(zh、en),地区决定货币、税费、可售商品、支付方式、法律条款。一个在德国的英语用户,语言是 en 但地区是 DE,要看英文界面加欧元价格加德国税率。所以标识要用 en-DE 这种组合,不能只用语言。
SEO 的关键是 hreflang:每个页面要声明所有语言版本的对应关系,并且互相引用(双向),还要有一个 x-default 指向语言选择页或默认版本。做对了搜索引擎才会给不同地区的用户展示对应版本,做错了会被判定为重复内容。
其他必须做的 SEO 项:每个语言版本独立的 title、description 和 canonical;结构化数据(商品页用 Product 标记价格和库存,视频页用 VideoObject),这直接影响搜索结果的富摘要展示;多语言 sitemap;robots.txt 要正确。
技术实现:语言包按需加载,不要把十几种语言全打进主包;SSR 时要根据 URL 而不是浏览器语言决定渲染哪个版本(否则爬虫拿到的语言不确定,SEO 直接废掉);不要用自动跳转强制改变用户语言——用户明确访问 /en/ 却被跳到 /de/,既伤体验又会让爬虫抓不到内容。检测到语言不匹配时给个提示条让用户自己选。
三个真实会踩的坑:文案长度差异——德语法语比中文长 50% 以上,按中文设计的按钮会被撑破,所以不能写死宽度;RTL 布局——阿拉伯语希伯来语要整体镜像,早期就用逻辑属性(margin-inline-start)能省大量后期改造;日期数字货币格式——用 Intl API 而不是手写,德国用逗号做小数点,日元没有小数位。
先明确指标,因为这三个指标对应的优化手段完全不同,笼统地「优化性能」没法落地。LCP(最大内容绘制)看主要内容多久出现,INP(交互到下一次绘制,已取代 FID)看交互响应速度,CLS(累积布局偏移)看页面有没有跳动。这三个是 Google 的排名因素之一,对官网来说直接影响 SEO。
LCP 优化,通常瓶颈是首屏那张大图或者主标题。手段:首屏关键图片用 preload 提前加载,并且不要给它加懒加载(很常见的错误,给首屏图加了 lazy 反而拖慢 LCP);图片用现代格式(WebP/AVIF)并按视口给多档尺寸(srcset);关键 CSS 内联,避免渲染被阻塞;服务端响应要快(SSR 加缓存、CDN 就近);字体用 font-display: swap 避免文字被字体加载卡住。
INP 优化,瓶颈是长任务占住主线程。手段:拆分长任务(大量数据处理分片执行或者放 Web Worker);减少首屏的 JS 体积(路由懒加载、组件按需引入);第三方脚本延后加载——统计、客服、广告这些是 INP 的头号杀手,要用 defer 或者交互后再加载;避免在滚动和输入事件里做重活。
CLS 优化,瓶颈是元素尺寸未知导致的跳动。手段:图片和视频必须写明宽高或 aspect-ratio(这一条能解决大部分 CLS);给广告位、动态内容预留固定高度的占位;不要在已有内容上方插入元素(比如顶部弹出的公告条会把整页推下去);字体切换用尺寸接近的 fallback 字体减少回流。
国际访问的额外重点:静态资源必须走全球 CDN,HTML 本身也要能缓存或者就近渲染;减少请求往返次数比减小体积更重要,因为跨洋 RTT 可能三百毫秒,三个串行请求就是一秒;用 preconnect 提前建立到 API 域名和 CDN 的连接(省掉 DNS、TCP、TLS 三次往返);上 HTTP/2 或 HTTP/3。
最后强调要看真实用户数据(RUM)而不只是 Lighthouse。Lighthouse 是实验室数据,在你的高配电脑和好网络上跑出来的分数没有代表性。要采集真实用户的 Web Vitals 并按地区、设备、网络分组看 P75——Google 的排名用的就是 P75。很可能整体分数不错,但某个地区的用户体验极差,这个只有 RUM 能发现。
这个页面的业务目标是转化(下载 App 或注册),不是单纯播放视频,所以设计上要围绕这个目标。同时它是从社交平台点进来的,要能被爬虫抓取生成预览卡片,这决定了它必须服务端渲染。
SEO 和社交预览:必须输出 Open Graph 和 Twitter Card 标签(标题、描述、封面图、视频地址),否则分享到社交平台只有一个干巴巴的链接,点击率差很多。还要输出 VideoObject 结构化数据让搜索引擎理解这是视频页。这些标签必须是服务端渲染在 HTML 里的,客户端动态设置对爬虫无效——这是个很常见的错误。
播放体验:H5 播放要注意几个平台限制。自动播放必须静音(各浏览器都限制带声音的自动播放),所以做法是静音自动播放加一个显眼的「点击开启声音」;iOS 需要 playsinline,否则会强制全屏播放;用 HLS 做自适应码率,跨国弱网下先给低清让它能播起来。封面图要作为 poster 提前加载,避免黑屏等待。
唤起 App是转化的核心,做法要分场景:已安装用 Universal Links(iOS)和 App Links(Android)直接唤起并跳到对应视频——注意这两个是系统级方案,比传统的 URL Scheme 可靠得多,而且在微信等封闭环境里 Scheme 基本被封了。未安装跳应用商店,并且要做延迟深度链接(deferred deep link):把视频 ID 通过剪贴板或者服务端指纹暂存,用户装完首次打开时自动跳到那个视频。这一步对转化率影响很大,做了和没做差别明显。
环境判断要细:在微信、Instagram、Twitter 这类应用内置浏览器里,很多能力被限制(无法直接跳商店、Scheme 被拦),需要引导「用浏览器打开」。这块只能靠 UA 判断加各平台的实际测试,没有优雅方案。
性能:这个页面来自社交流量,用户耐心极低,首屏必须极快。做法是页面做得非常轻(不要引入整个组件库和路由)、封面图优先、播放器脚本按需加载、SSR 输出的 HTML 直接可看。可以做成独立的轻量工程而不是塞进主站,因为主站的公共依赖对它是负担。
合规要注意:视频内容的地区可见性要在服务端判断,某些内容在某些国家不能展示,直接返回不可用页面而不是渲染出来再隐藏。
Web 端支付比 App 麻烦,因为它要跳出去再跳回来,而这个往返过程有大量丢失的可能:用户直接关了标签页、在渠道页面点了返回、跳回来的 URL 被浏览器拦截、或者干脆换了个浏览器完成支付。所以设计的核心是永远不要依赖回跳来确定支付结果。
主流程:服务端创建订单(定价、锁汇率)→ 创建支付单拿到渠道跳转地址 → 前端跳转(整页跳转或新窗口)→ 用户在渠道完成支付 → 渠道跳回我们的结果页 → 结果页轮询查询订单状态 → 展示最终结果。
关键设计点:结果页必须是「靠订单号查状态」而不是「读 URL 参数」。渠道回跳的 URL 参数完全不可信(用户可以手动构造),只能用来拿订单号,状态必须调自己的接口查。这一条是安全底线——有系统直接根据回跳参数里的 status=success 就发货,那等于把商品免费送人。
回跳丢失的兜底分几层:
一、跳转前把订单号存进本地(localStorage),并记录「有一笔待确认的支付」。用户回到站点任何页面时,检查到有待确认支付就主动查一次状态并提示。
二、订单列表和详情页每次进入都拉最新状态,用户找不到结果页也能自己看到。
三、邮件/站内通知:支付成功后服务端发通知,这在 Web 端尤其重要,因为用户可能压根不会回到你的站点。国际业务里邮件是主要触达手段。
四、服务端的补偿和对账兜底,这是最终防线。
新窗口 vs 整页跳转的取舍:新窗口能保留原页面(原页面可以轮询状态,体验更好),但可能被浏览器拦截弹窗,而且移动端浏览器的多窗口体验差。整页跳转最可靠但要处理好回跳。我的做法是桌面端开新窗口 + 原页轮询,移动端整页跳转 + 结果页轮询,两者都保留本地记录做兜底。
还有一个和 uni-app 端一样的坑:请求超时不能判定为失败。创建支付单的接口如果超时,可能后端已经创建成功了,此时按钮不能恢复成可点击状态,要进「确认中」并去查询,幂等号也要固定复用。这个逻辑在 Web 端同样必须做。
官网的核心价值是转化,所以埋点设计要围绕漏斗,而不是「把所有点击都记下来」——后者数据量巨大但没人分析。
先定义漏斗:以虚拟商品为例是「落地页访问 → 商品页浏览 → 点击购买 → 填写信息 → 发起支付 → 支付成功」。每一步都要有埋点,这样才能看出用户在哪一步流失。这个漏斗数据比任何技术指标都更能推动优化——比如发现 40% 的用户在支付环节流失,就该去查是不是某个地区的支付方式不支持。
要采集的维度:流量来源(UTM 参数、referrer,要在落地时就存下来并贯穿整个会话直到下单,这样才能算出各渠道的 ROI)、地区和语言、设备和浏览器、以及用户标识(未登录用匿名 ID,登录后要能关联打通,否则登录前的行为就丢了)。
技术实现:曝光用 IntersectionObserver 而不是滚动计算;用声明式埋点(给元素加 data 属性,由统一的采集器处理)而不是在业务代码里到处写上报调用,后者会让代码越来越脏还容易漏;批量合并上报,不要一个事件一个请求;页面关闭前用 sendBeacon 保证数据发得出去(普通请求在页面卸载时会被取消)。
技术监控也要一起做,和业务埋点共用通道:JS 错误、接口成功率和耗时、Web Vitals、资源加载失败。其中「接口失败率按地区拆分」对国际业务特别关键,某个国家访问异常在全局指标上是看不出来的。
几个实践注意点:埋点不能影响性能(异步、失败静默、不阻塞主流程);要有采样策略(错误全量、行为按比例);GDPR 合规——欧盟用户需要先获得 cookie 同意才能采集,没同意之前只能采匿名的必要数据,同意记录本身也要留痕。这一条不是可选项,违反了会有实质罚款,而且很多团队做国际站时会漏掉。
最后是埋点要有文档和版本管理。埋点最大的问题不是技术,而是过了半年没人知道某个事件是什么含义、字段有没有变过。所以要维护埋点字典,并且改动要走评审。
先明确前端安全的定位:服务端校验是根本,前端做的是减少攻击面。但 XSS 这类确实是前端主责,因为它的载体就是页面。
XSS 的本质是把用户输入当代码执行了。Vue 默认对插值做转义,所以正常写模板是安全的。风险点集中在几处:v-html——直接渲染 HTML,如果内容来自用户(评论、商品描述、富文本),必须先用 DOMPurify 这类库净化,白名单标签和属性,绝对不能自己写正则过滤(一定会被绕过);动态属性——把用户输入拼进 href 可能被注入 javascript: 协议,要校验协议;动态生成的样式和脚本;服务端渲染时把数据直接拼进 HTML(SSR 特有的风险,要正确转义)。
更彻底的防护是 CSP(内容安全策略),通过响应头限制脚本只能从指定来源加载、禁止内联脚本和 eval。这样即使有 XSS 注入点,脚本也执行不了。CSP 的落地难点是会打破很多第三方脚本和内联样式,所以要先用 report-only 模式收集违规报告,逐步收紧。国际官网通常会挂不少第三方(统计、客服、广告),这个梳理工作量不小但值得做。
CSRF 是利用你已登录的身份让浏览器自动带上 cookie 发起请求。防护手段:cookie 加 SameSite=Lax 或 Strict(现代浏览器默认 Lax,已经挡掉大部分场景);CSRF token(服务端下发、前端提交时带上、服务端校验);关键操作校验来源(Referer/Origin)。如果 token 放在 Authorization 头里而不是 cookie 里,CSRF 天然就不成立,因为浏览器不会自动带上自定义头——这也是现在前后端分离项目普遍的做法。
其他必要项:token 存储——放 localStorage 有 XSS 风险(脚本能读),放 HttpOnly cookie 能防 XSS 读取但要处理 CSRF,各有取舍,实践上重要系统会用 HttpOnly cookie 加 SameSite 加短有效期;敏感信息不落前端,卡号 CVV 用渠道托管页面或 SDK 收集(PCI-DSS 要求);依赖安全——定期扫描 npm 依赖漏洞,供应链攻击(一个被投毒的小依赖)在前端是真实威胁;不要在前端代码里放密钥,打包后照样能被找到。
最后一条容易忽略:点击劫持(clickjacking),用 X-Frame-Options 或 CSP 的 frame-ancestors 禁止自己的页面被嵌进别人的 iframe,支付相关页面尤其要设。
先判断要不要拆。官网通常包含几块性质差异很大的内容:营销和落地页(变更频繁、追求性能和 SEO)、交易流程(逻辑复杂、要求稳定)、用户中心(纯 SPA)、可能还有独立的活动页。如果都塞在一个工程里,一次营销页的改动要走整个交易系统的发布流程和回归测试,效率很低。
拆分方式按成本从低到高:一个仓库多入口(Monorepo 加多个构建产物)——最简单,共享代码方便,适合团队不大的情况;多个独立工程加共享组件库——各自独立发布,通过 npm 私有包共享 UI 和工具;微前端——运行时集成多个应用,适合多团队并行、技术栈不统一、或者需要渐进式迁移老系统的场景。
不要为了「架构先进」上微前端,它引入的复杂度是实打实的:样式隔离、JS 沙箱、公共依赖版本冲突、应用间通信、路由协同、以及调试和部署链路变长。团队只有几个人的时候,Monorepo 多入口就够了。这个判断力比会用微前端更重要。
必须做好的公共层:UI 组件库(统一视觉和交互,同时是国际化和无障碍的落地点)、请求层封装(统一鉴权、错误处理、重试、埋点)、国际化方案(语言包管理和加载策略要统一,不然每个应用一套会乱)、埋点和监控 SDK、工具函数和类型定义(金额格式化、日期处理这类在国际业务里极易出错的逻辑必须只有一份实现)。
构建和发布:用 Vite 提升开发体验;依赖版本要统一管理(Monorepo 用 pnpm workspace,多仓库要有版本对齐机制);构建产物按内容哈希命名并配长期缓存;要有构建体积监控,体积异常增长时在 CI 阶段就拦住,而不是上线后才发现。
发布策略:静态资源和 HTML 分离部署(资源先上 CDN 再更新 HTML,避免新 HTML 引用了还没上传的资源);支持灰度和快速回滚;国际业务要注意多地区 CDN 的刷新一致性——各地区节点更新有时差,可能出现部分用户拿到新 HTML 旧资源的情况,所以资源要保留旧版本一段时间不要立刻删。
权限要分四个层次,缺一层就有漏洞:菜单权限(能看到哪些入口)、路由权限(能访问哪些页面)、操作权限(页面里能点哪些按钮)、数据权限(能看到哪些数据行)。前三层是前端配合,第四层必须在后端做。
模型用 RBAC:用户关联角色,角色关联权限点。权限点要细到操作级别(order:refund 而不是只有 order)。国际业务还常要加地区维度——某个客服只能处理欧洲区的订单,这就是数据权限的一种。
前端实现:登录后拉取用户的权限点列表和菜单树。路由要动态注册(router.addRoute),只把有权限的路由加进去,这样直接输入 URL 也进不去。按钮级用自定义指令(v-permission="'order:refund'")或者一个包装组件来控制显示。
动态路由有个必踩的坑:addRoute 加的路由存在内存里,刷新页面就没了。所以要在路由守卫里判断「权限数据是否已加载」,没有就先拉取并重新注册,注册完用 next({ ...to, replace: true }) 重新进入一次。漏了这一步的表现是「刷新后白屏或跳回首页」,很多人卡在这里。
必须强调的一点:前端权限只是体验优化,不是安全措施。藏起来的按钮用户照样可以直接调接口,改一下本地的权限数据就能让菜单全部显示出来。所以每个接口都必须在后端独立鉴权,前端做的只是「不让用户看到点不了的东西」。这个认知说出来能明显区分对安全有没有概念。
数据权限的实现在后端:通常是在查询时自动追加条件(用户只能看自己部门的、自己地区的、自己创建的)。前端要配合的是不要把不该展示的字段暴露在接口响应里——有些实现是后端返回全量数据、前端隐藏部分字段,那等于没有权限控制,打开控制台就全看到了。
几个实践细节:权限变更后要能让用户的权限及时失效(token 里不要直接存权限,或者提供强制刷新机制);要有超级管理员的兜底但操作要留审计;权限点的命名和管理要规范,否则几百个权限点没人搞得清;敏感操作要二次确认加审计日志(谁在什么时候退了哪笔款)。
先判断该不该做成动态的。如果表单结构固定,直接写死是最简单可维护的;只有当表单结构需要由配置或后端决定时才上动态方案——典型场景是不同商品类型有不同的属性字段、不同国家的收货地址和税号格式不同、运营要能自己配活动报名表。
核心是 JSON Schema 驱动:用一份配置描述字段(类型、标签、校验规则、默认值、是否必填、可选项来源),前端有个渲染引擎遍历配置生成表单。这样加字段不用发版,改配置就行。
难点在联动。字段之间的依赖关系有几类:显隐联动(选了「企业」才显示税号)、选项联动(选了国家才加载对应的省份列表)、校验联动(结束时间必须晚于开始时间)、值联动(数量和单价变化时自动算总价)。实现上要在配置里表达依赖关系(用表达式字符串或者规则对象),渲染引擎负责监听依赖字段的变化并触发对应行为。要小心循环依赖,A 影响 B、B 又影响 A 会导致死循环,配置层面要能检测。
校验要分层:同步校验(必填、格式、长度)在配置里声明;异步校验(用户名是否重复)要防抖并处理竞态(先发的请求后返回会覆盖新结果,要用版本判断);跨字段校验放在表单级别。而且前端校验必须在后端重复一遍,前端校验只是提升体验。
国际化的额外要求:字段的标签和错误提示要走 i18n;不同地区的地址格式差异很大(美国要州和 ZIP、日本邮编在前、有些国家没有省级行政区),所以地址表单本身就该是按国家动态生成的;电话号码格式要按国家校验;姓名字段不要强行拆成姓和名(很多文化里顺序不同甚至没有这个概念)。这几条是国际化表单最容易做错的地方。
性能:字段很多时(几十上百个)要注意避免全表单重渲染——每个字段做成独立组件,只有自己的值变化才更新。大表单还要考虑分步(Wizard)或者折叠分组,一次渲染几百个输入框会明显卡。
实践建议:不要自己从零写渲染引擎,社区有成熟方案(FormKit、Vue Formily 等)。自研的成本主要在联动和校验的边界情况上,会持续消耗维护精力。除非需求确实特殊,否则优先用现成的。
第一个问题应该是「为什么要在前端展示几万行」。用户不可能真的去看几万行,这个需求背后通常是「需要查找」或者「需要导出」。所以先问清场景——能用搜索和筛选解决的就不要渲染全量,能用后端导出解决的就不要前端处理。这个反问比直接给技术方案更能体现判断力。
确实需要展示的话,核心手段是虚拟滚动:只渲染可视区域内的几十行 DOM,滚动时动态替换内容,用占位元素撑起总高度。这样 DOM 节点数从几万降到几十,是唯一能根治的办法。现成方案有 vue-virtual-scroller,或者用 VXE Table、AG Grid 这类内置虚拟滚动的表格库。
虚拟滚动的难点要说清:不定高行(内容长度不同导致行高不一致)需要动态测量和缓存高度,实现复杂度高一个档;横向也要虚拟化(列很多时);固定列和表头要和滚动同步;合并单元格和虚拟滚动几乎不兼容。所以能固定行高就固定,能减少列就减少。
数据层面的优化:分页加载(滚动到底部加载下一页)比一次拉全量好;后端只返回需要的字段;大数据量的排序和筛选放后端做,前端排序几万条会卡死主线程;如果必须前端处理,用 Web Worker 或者分片处理避免阻塞。
渲染层面的优化:单元格内容尽量简单,避免每个单元格都是复杂组件(一个带头像、标签、下拉菜单的单元格乘以几百个可见单元格就很重);用 v-memo 或者拆分组件减少重渲染;避免在模板里做计算,预处理好数据。
交互功能的取舍:全选、批量操作、行内编辑这些在虚拟滚动下都要特殊处理(不在视口内的行没有 DOM)。全选要用「选中所有」的语义而不是遍历勾选每一行,批量操作把条件传给后端而不是传 ID 列表。
导出不要在前端做:几万行生成 Excel 会占用大量内存甚至崩溃,而且拿不到全量数据。正确做法是后端异步生成文件,前端轮询进度然后下载链接。这也是「前端不该做的事就不要做」的例子。
分四层来设计,各层的目标和失效方式完全不同,混在一起想就乱了。
一、HTTP 缓存(浏览器和 CDN)。这是收益最大的一层。带内容哈希的静态资源(JS、CSS、图片)设长期强缓存(Cache-Control: max-age=31536000, immutable),因为内容变了文件名就变了,不存在失效问题。HTML 必须不缓存或短缓存(no-cache),否则用户会一直拿到旧的 HTML 引用旧资源,新版本发不出去。这个「HTML 不缓存、资源长缓存」的组合是标准做法。
这里有个国际业务的坑:多地区 CDN 的缓存更新有时差,可能出现用户拿到新 HTML 但某个地区的 CDN 还没有对应的新资源,导致加载失败白屏。所以旧版本资源要保留一段时间不能立刻删,并且动态导入要有失败重试。
二、接口数据缓存。要按数据特性分类:基础配置类(分类、地区列表、汇率)变化极少,可以缓存较长时间甚至持久化到 localStorage;业务数据(商品详情、库存)要短缓存或者不缓存;用户私有数据(订单、余额)绝对不能缓存到可被他人读取的地方,而且要在登出时清理。
实现上可以用 SWR 模式(stale-while-revalidate):先返回缓存让页面立刻有内容,同时后台请求新数据更新。这个模式的体验很好——秒开加自动刷新。要注意给用户一个「数据可能不是最新」的暗示,或者更新后有个平滑的过渡。
三、状态缓存(页面级)。用户从列表页进详情页再返回,列表的滚动位置、筛选条件、已加载的数据应该保留。用 keep-alive 加上把筛选条件同步到 URL query——后者更重要,因为它同时解决了「刷新后状态丢失」和「链接可分享」两个问题。要注意 keep-alive 的内存代价,缓存页面数要设 max,否则用户逛久了内存持续增长。
四、本地持久化。localStorage 存少量配置和 token;IndexedDB 存较大的结构化数据(离线草稿、大列表缓存)。要有版本管理和容量控制——结构变了旧数据要能迁移或清理,否则升级后读到旧结构会报错;容量满了写入会失败,要 catch 住。
缓存的一致性兜底:所有缓存都要有过期时间,即使失效逻辑出问题也不会永久错误;用户主动刷新要能绕过缓存(下拉刷新、刷新按钮强制拉最新);关键操作后要主动失效相关缓存(下单成功后清掉库存缓存)。
这个场景有三个叠加的约束,任何一个单独出现都不难,凑在一起就必须重新设计:点位数量大、数据在高频变化、页面要连续运行十几个小时。第三个是最容易被忽略也最容易出事的。
一、海量点位不能用 DOM 或地图 SDK 的默认 Marker。几千个 Marker 意味着几千个 DOM 节点或图层对象,创建慢、更新慢、内存高。要改成 Canvas 自绘——一层覆盖在地图上的画布,自己画点。
配套三件事:网格聚合(相近的点合成一个带数字的簇。注意它的首要目的是可读性不是性能——密集区域几百个点重叠在一起,人根本看不出有多少,聚合之后才有信息量)、视野裁剪(只画当前视口内的点,地图缩放和平移时重算)、帧内合并重绘(位置更新可能每秒来几十次,不能来一次画一次,要在下一帧统一画一次)。
二、点击要能命中,这是 Canvas 方案的主要代价。DOM 有原生的事件,Canvas 只有一张图。做法是维护一个空间索引(网格或四叉树),点击时按坐标查索引找到最近的点。要设一个命中半径,否则用户要精确点在几像素的圆点上,体验很差。
三、实时数据用长连接推增量,不要轮询全量。几千个骑手每次全量拉是几百 KB,轮询会把带宽和服务端都打满。推送只发变化的点位。
但长连接带来两个必须处理的问题:重连后要做一次全量对齐——断线期间的增量丢了,只靠增量会让页面上的状态永久偏离真实。以及要区分「没有变化」和「连接断了」:两者在界面上都是「点位不动」。所以要有心跳,超过阈值没收到任何消息就判定为异常。
四、长时运行是这题的核心,也是最能区分候选人的地方。开一整天意味着任何微小的泄漏都会累积成事故。四条:
注册即清理。所有定时器、事件监听、动画帧、长连接,在组件卸载时必须成对清理。最容易漏的是地图 SDK 的事件监听和 resize 监听。
数据窗口要有上限。异常订单列表如果只追加不淘汰,跑一天就是几万条在内存里。要设条数上限,超出丢弃最旧的(需要历史就去查接口,不要都存在前端)。
数据陈旧必须显式提示。这一条我认为是最重要的:宁可显示「数据已停止更新 3 分钟」,也不要显示一个看起来正常但其实是旧的数字。值班同学是靠这个看板做调度决策的,一个静默失效的看板比没有看板更危险——他会基于错误信息派单。
跨天和时钟。页面跨过零点,日期相关的统计要能正确切换;系统休眠唤醒后定时器的行为不可靠,唤醒时要主动触发一次全量对齐。
五、降级要可逆。点位太多时自动降级(提高聚合粒度、降低刷新频率),但要在界面上标明当前处于降级状态,并且条件缓解后能自动恢复。不可逆的降级会导致「用了一会儿之后就一直是模糊模式」,而用户不知道为什么。
怎么验证:点位性能压一个数量级的量(几千到上万)看帧率;长时运行必须真的挂机跑几小时然后看堆快照有没有持续增长——这类问题只能靠长时间运行暴露,短时测试一定测不出来;断连恢复要手动断网再恢复,验证全量对齐和陈旧提示都生效。
先认清这类需求的根本前提:查询不是我们写的,是用户拖出来的。固定报表的 SQL 是开发写的、测过的、知道大概多快;自助分析里用户可以任意组合,而他不知道也不需要知道数据量。
核心手段是提交前做代价预估并阻断。按各维度的基数估算结果行数(维度基数相乘是上界)与大致扫描量,超阈值直接阻断、接近阈值给警告。预估方法我会选粗糙的「基数相乘」而不是走数据库执行计划——执行计划更准但要真把查询发过去解析、成本不低且不同引擎接口差异大;而我要拦的是「量级明显不对」,不是精确预测耗时,够用就好。
报错必须指出是哪个维度导致的并给替代建议:「你选的『用户 ID』有几百万个不同值,请改用『用户等级』或加更严格的筛选」——只说「查询过大」用户不知道该改什么。
第二个必须做的是「取消要真的取消服务端执行」。只在前端丢弃响应是一个视觉上的取消:用户点了取消又改配置重新提交,服务端就同时跑着两个查询而他完全不知道;在多租户共享查询资源的场景下这会放大成「一个用户拖慢整个租户」。而「可取消」本身也是防重复提交的关键——没有取消按钮,用户等不了只能刷新页面,而刷新不会取消服务端查询。
第三是指标的聚合方式要做成元数据,不能靠前端按名称猜。用户把「转化率」拖进来按渠道分组,如果对各渠道的转化率求和就会得出超过 100% 的数字,而用户会拿着截图来质疑我们的数据准确性。把分析能力开放给非技术用户时,「用错工具产生的错误结果」会被归因为「你们数据不准」——所以拦住错误用法比事后解释重要得多。
结果集要分页加懒加载,不要一次取全量(几十万行会把标签页搞崩);大结果集的导出走异步任务——「页面展示」和「数据导出」是两个不同的需求,混在一起两边都做不好。
是两类不同的问题,先分清才不会用错方案。普通大表格是一维列表:行是记录、列是固定字段,虚拟滚动只需要纵向。透视表是二维交叉:行和列都由用户拖出来的维度决定——行头多级、列头多级、行头列头都要跨行跨列合并、而且列数不确定,所以横向也要虚拟滚动。
最容易翻车也最危险的是表头错位。维度层级从两级变三级时,如果表头的跨度计算和主体的列定位是两套逻辑,很容易只改对一处,结果「一月」那一列下面显示的是二月的数据——而界面看起来完全正常,只是数字对错了位置。正确做法是把表头结构(层级、节点、跨度、合并范围)在数据到达后一次性预计算,主体的单元格定位用同一份结构——共用一份结构就不可能错位。错位类 bug 比崩溃类危险得多:崩溃会立刻被发现,错位会让用户默默用错数据做决策。
第二个透视表特有的难点是合并单元格在虚拟滚动下的裁剪。如果裁剪逻辑是「取可见行范围内的单元格」,一个跨五行的合并块在起始行滚出视野后就不再被渲染,那一片区域变成空白。正确做法是按「与可见范围有交集」判断并对部分可见的合并块做偏移渲染——虚拟滚动的裁剪单位必须和渲染单位一致,合并单元格的渲染单位是「合并块」不是「行」。
第三是冻结区同步:冻结行头、冻结列头、主体是三个渲染区域,滚动位置必须由主体单一驱动,让它们各自监听滚动事件会有时序差,快速滚动时能明显看到左右错行——而慢滚可能测不出来。
第四是列宽要一次性测算后固定:如果按当前可见数据自适应,横向滚动时新列进入导致列宽重算,已渲染列位置全部偏移、用户正在看的单元格会跳走。虚拟滚动场景里布局稳定优先于内容自适应。
还有一条判断题:为什么不用 Canvas 画(性能上限更高)——因为报表用户要复制单元格内容到别处用,而 Canvas 会丢掉文本选择与复制。依据是「用户的核心操作是什么」而不是性能上限。
分享只能传查询配置,绝不能传结果快照——传快照就是越权。因为快照里的数据是按分享者的权限查出来的:区域负责人把他的区域报表分享给一个只能看单个部门的同事,那个同事就看到了整个区域的数据。正确做法是查看者打开时按他自己的权限重新执行查询,同一个链接不同人看到的范围不同——这正是想要的行为。
一般化的教训:任何「把查询结果持久化并传播」的功能都要先问「这份结果是按谁的权限查的」——快照、缓存、导出文件、消息推送内容,都是同一个问题的不同形态。
但这会带来一个必须正面处理的产品问题:两个人打开同一个链接看到的总额不一样,他们在会上对着不同的数字互相质疑,最后来问「你们的数据是不是错了」。解法是在界面上显式展示当前的数据范围(「本报表基于你管辖的 3 个部门」)——不改变任何数据逻辑,但把隐形的困惑变成明确的说明,消掉一整类工单。
另一个容易漏的点:不要泄露「被过滤数据的存在」。显示「另有 12 条数据你无权查看」看起来是友好提示,但这个计数本身就是信息泄露——从「另有 3 个部门」能推断出组织规模。权限设计里「不该看到的数据,连它的存在都不该被感知」,这条比「不返回数据内容」严格一层。而「本报表基于你管辖的 3 个部门」这种正向表述是安全的——说清自己的范围可以,说出范围之外有多少就是泄露。
最容易漏权限的地方是非主路径的数据出口:导出接口、定时订阅、开放 API、以及为性能做的旁路(预聚合表、缓存、只读副本)。权限过滤必须在数据出口的唯一收敛点实现,散落在各接口里一定会漏一个,而漏一个就等于全部白做。还要保证汇总与明细在同一权限范围内计算——否则用户看到「总额 100 万但下钻明细只有 60 万」,这不只是困惑,它泄露了「有 40 万是你看不到的」。
问题的性质是「配置结果不可预测」:打包可售性是各子项可售性的交集,子项之间还有日期与人数联动,这些配错了包依然能保存成功,但它永远不可售——而且没有任何提示。结果是包在前台搜不到、或点进去不可售,排查要跨好几个系统,运营完全搞不定,每次都要找技术。
核心解法是在配置页内置试算:运营选一个具体的出行日期与人数,实时返回这一天的可售性与打包价。
而试算最重要的输出不是「可售/不可售」,是「不可售的原因定位到具体子项」。只说「不可售」运营什么也做不了;说「第三天酒店 A 无余量」他才知道该换酒店还是改日期。这一条对接口有要求——后端在返回不可售时必须带上「是哪个子项、什么原因」,如果只返回一个布尔值前端做不出这个能力。前端的诊断能力上限取决于接口给了多少信息,遇到这类需求要先去谈接口而不是在前端猜。
试算必须调服务端算,不能在前端实现一套:前端算会和真实的可售性判定分叉,而分叉的试算器比没有试算器更危险,因为运营会信任它。
配置模型上有两条容易踩的:包内角色必须显式化(去程与返程机票同属机票品类但联动规则不同,靠子项顺序隐式表达的话,运营调一下顺序联动就绑错了,包变成永久不可售而他不知道为什么);人数到房间数的换算结果要显示出来给运营确认(「3 人 → 1 间三人房」——这里最容易错,而错了的后果是用户到店住不下)。
还有一个产品判断:运营真正的问题是「这个包在下个月哪些日期可以卖」,而单日试算要点三十次、实际没人这么干。工具要匹配用户真正的问题,而不是匹配问题的最小单元——所以要支持按日期区间批量试算并用日历形式展示。
第一条:不要用「每个语言一个页签」——它是最自然的设计,也是最容易掩盖缺失的设计。页签式的致命问题是每个页签单独看都很正常,看不出哪些语言是空的:运营编辑完中英文就发布,其他六种空着,前台在小语种地区有值的字段用当地语言、没值的回退英文,出现德语页面里夹着英文段落。改为源语言与目标语言并排对照、同一字段上下对齐,缺失一眼可见。多版本数据的编辑界面必须让「版本之间的差异」可见,而不是让用户逐个版本看。
第二条也是收益最大的:为每个字段维护独立的翻译状态,并在源文变更时自动标记受影响的目标语言为「待重译」。不这么做的话,运营改了中文描述,八种译文全是旧的而界面上没有任何提示,只能靠他记得去改——而这是记不住的。我们踩过:更新了酒店政策的中文,其他语言还是旧政策,用户按旧译文下单、到店发现规则不一样。
状态必须是逐字段的而不是整条内容一个状态:整条一个状态(「德语版已完成」)表达不了真实情况(名称译好了、描述没译、政策是机翻);更关键的是源文改的是某个字段,只有字段级状态才能精确标记受影响的范围——状态的粒度必须匹配变更的粒度。
第三条是术语一致性:接术语库校验专有名词(品牌名、地名、房型名)的标准译法,但只做提示不做强制替换——语言不是查找替换能处理的,同一个词在不同语法位置需要不同形态,强制替换会产生比不一致更奇怪的文案。
第四条是发布前的结构校验,这个最容易漏:译文里的标签与占位符数量必须与源文一致。我们踩过——富文本里有个「{城市名}」占位符,译员翻译时把它删掉了,前台渲染时那个位置显示了原始的花括号文本。带结构的文本在翻译流程里会被破坏结构,必须有结构层面的校验——只校验「有没有内容」是不够的。
还有一个国际化特有的布局问题:德语俄语的译文明显更长,房型名在卡片上被截断成看不懂的样子。所以要在并排编辑里显示各语言的字符数差异,并让预览用真实的前台渲染——国际化的布局问题必须在内容生产阶段就暴露,等到前端做自适应截断是治标,因为被截断的内容本身就传达不了信息。
先靠监控数据把范围切出来,别急着看代码。白屏要有专门的上报(比如页面渲染完成打点,超时未打点视为白屏),然后按维度切分。
集中在某个浏览器或版本→ 兼容性问题。最常见的是用了新语法或新 API 但构建目标没覆盖到老浏览器,代码直接语法报错导致整个 bundle 执行失败。这类问题的特征是「新版本发布后一部分用户全白」。要检查构建的 target 配置和 polyfill,以及第三方依赖是否发布了未编译的 ESM 代码(这个很隐蔽——你的代码编译了,但某个依赖包里的新语法没被处理)。
集中在某个地区→ 资源加载失败。CDN 在该地区的节点异常、或者某个域名被当地网络屏蔽、或者证书问题。查该地区的资源加载成功率。
集中在某个时间点之后→ 发布引入。直接看那次发布改了什么,优先回滚而不是继续查,先恢复再定位。
不集中,随机出现→ 通常是资源加载失败的偶发(网络抖动导致某个 chunk 没下载下来,动态导入失败直接白屏)或者接口返回异常数据导致渲染报错。
几类具体原因和对应处理:chunk 加载失败——路由懒加载的 chunk 404 或超时,这在发布后尤其常见(用户还停留在旧页面,旧 HTML 引用的 chunk 已经被新版本删了),处理办法是保留旧版本资源一段时间,并且给动态导入加失败重试和兜底刷新。接口数据结构意外——某字段为 null 但代码直接取了它的属性,这类要用可选链和默认值做防御。SSR 水合失败——服务端和客户端渲染结果不一致,导致 Vue 丢弃 DOM 重新渲染或者直接报错,常见原因是用了 window、随机值、或者时间。
必须补上的能力:全局错误捕获(window.onerror、unhandledrejection、Vue 的 errorHandler)并上报带堆栈和用户操作路径;sourcemap 管理——线上代码是压缩的,没有 sourcemap 堆栈是天书,要上传到监控平台但不要部署到公网;错误边界——某个组件报错时降级成占位而不是整页白掉;白屏自愈——检测到长时间无内容时自动重载一次并上报。
先看 RUM 数据确认「慢在哪一段」,用真实用户的性能数据按阶段拆开:DNS、TCP、TLS、TTFB、资源加载、渲染、可交互。哪一段变长了,方向就清楚了。只用 Lighthouse 跑一遍是不够的,它反映不了真实用户的网络和设备分布。
TTFB 变长→ 服务端或网络问题。SSR 服务变慢(渲染耗时增加、缓存命中率下降、或者依赖的 API 变慢)、CDN 回源率上升、或者某个地区的链路劣化。SSR 场景下最常见的是它依赖的后端接口慢了,因为服务端渲染要等数据才能输出 HTML,一个慢接口会直接拖长 TTFB。所以 SSR 一定要给接口设超时并有降级(超时就输出不含该模块的页面,客户端再补)。
资源加载变长→ 体积变大或者请求变多。查构建产物体积的变化:是不是新引入了一个大依赖、是不是本该按需引入的组件库被全量打进来了、是不是某个依赖升级后体积暴涨、是不是 tree shaking 失效了(比如引入方式变了,或者依赖包标了副作用)。这里的关键是要有体积基线和 CI 拦截,否则体积是慢慢涨上去的,等发现时已经积累了很多。
渲染和可交互变长→ 主线程被占住。查有没有新增的第三方脚本(这是最高频的原因——市场部加了个营销工具、客服系统换了个供应商,一个同步加载的第三方脚本能让 INP 直接崩掉)、有没有新增的首屏计算逻辑、组件数量是否暴增。
别忘了非代码原因:图片是不是换成了未压缩的大图(运营上传的原图)、字体文件是不是变大了、CDN 配置是不是被改过(缓存策略、压缩没开)、有没有新加的重定向(多一次跳转就多一个 RTT)。国际业务里还要看是不是某地区的 CDN 节点变化。
排查方法上,最有效的是对比:拿变慢前后的两个版本,用同样的条件跑 WebPageTest 或者本地 Lighthouse 对比瀑布图,差异一眼就能看出来。同时查发布记录和配置变更记录——突发的性能劣化几乎总能追溯到某次变更。
长期防护:把性能指标纳入 CI(体积预算、Lighthouse 分数门槛)、线上 RUM 监控加告警(按地区看 P75)、第三方脚本必须评审并且默认异步加载。
先用搜索引擎自己的工具确认现象,不要凭感觉。Google Search Console 能看到收录数、抓取错误、索引被排除的原因,这是最权威的信息源,绝大多数问题在这里能直接看到原因。
按可能性排查。
一、抓取被阻止了。检查 robots.txt 是不是误禁了(发布时把测试环境的配置带上线,这个错误很常见且后果严重)、页面有没有误加 noindex 标签、是不是有 WAF 或防爬规则把搜索引擎的爬虫也拦了(要按 UA 和 IP 段放行官方爬虫)。
二、爬虫抓到的是空页面。这是 SPA 的经典问题——如果改动把某些页面从 SSR/SSG 变成了纯客户端渲染,爬虫拿到的 HTML 里没有内容。虽然 Google 能执行 JS,但渲染有配额和延迟,不保证抓全,其他搜索引擎能力更弱。验证方法是直接 curl 看返回的 HTML 里有没有正文,或者用 Search Console 的「网址检查」看渲染结果。
三、URL 结构变了但没做重定向。改版时路径变化必须做 301 永久重定向,否则旧 URL 全部 404,积累的权重直接归零。这是改版最大的 SEO 风险。
四、canonical 或 hreflang 配错了。比如所有语言版本的 canonical 都指向了英文版,那其他语言版本就都不会被单独收录;或者 hreflang 不是双向互指,被判定为重复内容。多语言站这块很容易配错。
五、内容和性能因素。大量页面内容重复或过于稀薄(比如自动生成的筛选页产生成千上万个近似页面)会被降权;Core Web Vitals 太差也是排名因素;站点被判定为有安全问题(挂马、恶意跳转)会被直接警告。
六、外部因素。搜索引擎算法更新(这个只能观察行业整体变化来判断)、或者被恶意 SEO 攻击(大量垃圾外链)。
处理和长期建设:先修最致命的(robots、noindex、重定向),这些是一改就能恢复的。然后把 SEO 关键项纳入发布检查——每次改版自动校验 robots、canonical、hreflang、title、结构化数据是否正常,以及关键页面的 HTML 里有没有正文。SEO 的问题特点是发现滞后、恢复也滞后(收录恢复要几周),所以事前的自动校验比事后排查有价值得多。
按「服务端状态对不对 → 前端有没有查 → 查到了有没有用上」三段来查,这个顺序最快。
第一段:直接调订单查询接口看服务端状态。如果服务端也是待支付,问题在后端链路(渠道回调没到、验签失败、消息没消费),走后端的排查流程。如果服务端已经是已支付,问题在前端。
第二段:前端有没有去查。Web 端最常见的断点是回跳丢失——用户在渠道页面完成支付后没有正常跳回来(关了标签页、点了返回、跳转被拦截、或者换了浏览器完成支付),结果页压根没被打开过,轮询逻辑从未执行。
这就是为什么不能只依赖结果页轮询。要检查兜底机制是否生效:本地是否记录了「待确认支付」、用户回到站点时有没有主动查、订单列表进入时有没有拉最新状态。如果这些兜底压根没实现,那这就是设计缺陷而不是 bug。
第三段:查到了但没用上。几种情况:读到了旧数据——后端读写分离的主从延迟,或者接口有缓存没失效,特征是「刷新几次就好了」;前端状态没更新——页面被缓存(keep-alive)而没有在激活时重新拉数据、或者响应式没触发;轮询逻辑有问题——次数用完就停了但没给出正确提示、组件卸载时没清理定时器、或者页面切到后台被浏览器节流(后台标签页的定时器会被降频,这个在 Web 端很容易踩)。
还有一个 Web 特有的:回跳参数被信任了。如果结果页是根据 URL 上的 status 参数展示状态,而渠道回跳时带的参数不符合预期(或者用户手动改了),就会显示错误状态。正确做法始终是拿订单号查自己的接口。
改进方向:把确认机制做成多重保障——结果页轮询、站内任意页面检测待确认支付、订单列表强制刷新、邮件通知、服务端补偿任务。任何一路通了状态就是对的。另外状态展示要有中间态,「支付成功,正在确认」比停在「待支付」好得多,用户不会因为看到「待支付」而重复付款。
后台管理系统这类单页应用长时间不刷新的场景最容易出这个问题,用户开着标签页一整天,内存从几十兆涨到几个 G。
先确认是泄漏还是峰值高:反复进出同一个页面,用 Chrome DevTools 的 Memory 面板看内存是否回落到基线。不回落就是泄漏。具体做法是取一次快照、操作若干次、强制 GC、再取一次快照,用 Comparison 视图看哪类对象数量在持续增长。
Vue 项目里的典型泄漏源,按频率排:
事件监听没移除。在 onMounted 里给 window 或 document 加了 resize、scroll、keydown 监听,卸载时忘了 removeEventListener。因为监听器的回调闭包引用了组件实例,整个组件树都释放不掉。特征是反复进出页面后同一个事件触发了 N 次回调。
定时器没清理。setInterval 忘了 clear,或者 setTimeout 的递归调用没停。
第三方库的实例没销毁。图表(ECharts)、地图、编辑器、播放器这些都持有大量 DOM 和数据,必须在卸载时调它们的 dispose 或 destroy。这是后台系统最常见的泄漏源,因为图表很多而且占内存大。
全局状态只增不减。往 Pinia 或全局对象里塞数据而从不清理,比如把每个页面的列表数据都缓存起来。keep-alive 没设 max 也属于这类——缓存的页面越来越多。
闭包和引用意外持有。把组件实例或 DOM 引用存进了模块级变量、或者传给了长生命周期的对象(全局事件总线、单例服务)。
排查手段:Memory 面板的快照对比找出增长的对象类型,然后看它的 Retainers(保留路径)——这条引用链会直接告诉你是谁在持有它,通常一眼就能定位到代码。还可以用 Performance 面板录制一段操作看内存曲线,以及 Detached DOM 节点的数量(已从文档移除但仍被 JS 引用的节点,是泄漏的强信号)。
预防:养成「注册即写清理」的习惯,写 onMounted 里的监听时立刻把 onUnmounted 的清理写上;更好的做法是封装成组合函数(useEventListener、useInterval),在函数内部自动处理清理,业务代码不可能忘(VueUse 就是这个思路);keep-alive 一律设 max;上线后监控内存指标,长会话的应用尤其要看。
先确认数据到了哪一层:接口返回对不对(看 Network)→ 数据有没有赋值到状态(打日志或 Vue DevTools 看)→ 状态变了模板有没有重渲染。三段分开验证,别猜。
如果状态压根没变,问题在赋值逻辑:可能是异步竞态(先发的请求后返回,用旧数据覆盖了新数据)、可能是赋值在了一个错误的变量上、可能是 await 之后组件已卸载(mounted 检查后直接 return 了)。竞态是最隐蔽的,特征是「快速切换筛选条件时数据错乱」,解法是给请求加版本号,只接受最新那次的结果。
如果状态变了但页面没渲染,几种原因:
响应式丢失——这是最常见的。reactive 对象被整体重新赋值(state = newObj),视图绑定的还是旧的代理;或者解构了 reactive 对象,拿到的是普通值;或者把 ref 放进了普通对象里传递,丢了包装。Vue 3 用 Proxy 之后新增属性和数组下标赋值是能侦听的(Vue 2 不行,需要 $set),所以如果是老项目要注意这个差异。
computed 的依赖不对——计算属性依赖的不是响应式数据(比如依赖了一个模块级的普通变量),那个变量变了 computed 不会重算。
v-for 的 key 问题——用了不稳定的 key(index 或随机值),导致 Vue 复用了错误的节点,表现为「数据变了但显示的还是旧内容」或者「内容串位」。
组件被 keep-alive 缓存了——数据在别处更新了,但这个组件处于缓存状态没有重新执行获取逻辑。需要在 onActivated 里刷新。
子组件没有响应 props 变化——子组件在 onMounted 里根据 props 做了初始化,父组件传新值时没有重新执行。这是 Vue 里对应 didUpdateWidget 的问题,要用 watch 监听 props 变化。
排查工具:Vue DevTools 是最直接的——能看到组件树上每个组件的实际 state 和 props,一眼看出是数据没到还是数据到了没渲染。还可以用它的时间线看组件的渲染次数,判断有没有触发重渲染。
先用可视化工具看构成,别凭感觉猜。rollup-plugin-visualizer 或者 vite-bundle-analyzer 生成产物分析图,按体积排序看大头在哪。有基线的话和上个版本对比,差异一眼就能看出来。
体积变大的常见原因:
新引入了大依赖,而且往往是间接引入——某个小工具库依赖了 moment、lodash 全量、或者一个完整的 polyfill 集合。查 package-lock 的变化和依赖树。
按需引入失效。组件库本该按需引入,但某处写了 import ElementPlus from 'element-plus' 整包引入,或者按需引入的插件配置被改坏了。特征是某个 chunk 突然大了几百 KB。
Tree shaking 失效。可能是依赖包标了 sideEffects、或者用了 CommonJS 格式的包(无法静态分析)、或者引入方式变了(import * as 会阻止摇树)。
重复打包。同一个库的多个版本被打进来(不同依赖各自要求不同版本),或者代码分割配置导致公共模块被复制到多个 chunk 里。
静态资源被打进 JS。图片或字体的 inline 阈值设太大,几十 KB 的图片被 base64 内联进 bundle(base64 还会膨胀三分之一)。
sourcemap 或调试代码上线。生产构建误开了 sourcemap,或者没剔除 console 和调试模块。
构建变慢的原因不完全重叠:依赖数量增长(预构建变慢)、某个插件性能差(比如全量的类型检查、ESLint 在构建时跑)、源文件数量暴增、或者 node_modules 里有需要被转译的包。Vite 可以用 --profile 或 vite-plugin-inspect 看各插件耗时;Webpack 用 speed-measure-webpack-plugin。
关键是要有防线:把体积预算加进 CI——超过阈值构建失败或者告警,这样体积是「一次性被拦住」而不是「慢慢涨上去没人发现」。同时把依赖新增纳入 code review,引一个包之前先看它的体积和依赖树(bundlephobia 之类的工具能直接看)。
这类问题投诉的杀伤力很大(用户白干半小时),而且往往是多个小问题叠加而不是单一 bug。我会按「数据是否到了后端」分两条线查。
先确认后端有没有收到。查后端日志和数据库。如果收到了但用户看不到,是查询或展示问题(可能是主从延迟、缓存、或者跳转后的页面没拉新数据)。如果压根没收到,继续往前查。
没收到的常见原因:
请求失败但前端提示不明确。接口 500 或者超时了,前端只弹了个「提交失败」甚至静默失败,用户以为成功了就关掉页面。更糟的是超时——请求可能实际成功了但前端判定失败,用户重新填一遍导致重复提交。这和支付的超时问题是同一类。
校验拦住了但提示没看见。表单校验失败,错误提示在页面下方而用户没滚动到,看起来就是「点了没反应」。这是体验缺陷,要自动滚动到第一个错误字段并聚焦。
token 过期。用户填了很久,token 在此期间过期了,提交时被拦截跳转到登录页——表单数据全丢。这是最典型也最伤的场景。
页面被意外刷新或跳转。误触了浏览器返回、点了某个会跳转的链接、或者代码里有条件跳转逻辑被触发。
数据量超限。富文本或图片太大,超过了后端的请求体限制(Nginx 的 client_max_body_size),返回 413 但前端没处理这个状态码。
解决方案要分两层。
防丢失:本地草稿自动保存——表单内容定时(或防抖)写入 localStorage,进入页面时检测到草稿就提示恢复。这一条能解决绝大部分投诉,成本也低。离开页面前拦截——用 beforeunload 和路由守卫,有未保存内容时提示确认。token 无感刷新——不要让用户填表期间被踢出去,配合刷新 token 机制;即使真的过期了,登录后也要能回到原页面并恢复草稿。
提交要可靠:超时不能当失败,要提示「结果确认中」并去查询状态;提交要幂等(带幂等号),这样重试不会产生重复数据;失败要给出明确原因和可重试的入口,而不是笼统的「失败了」;提交中要禁用按钮但要有明确的 loading 状态。
这题答得好的关键是意识到「防丢失」比「修 bug」更重要——具体是哪个原因导致的因人而异,但草稿自动保存和离开拦截是通用的兜底,做了之后这类投诉会大幅减少。
这是国际站的高频问题,关键是要有分地区的可观测数据,否则你只能听到零散的用户反馈却无法定位。
先确认范围和现象:是完全打不开还是慢?是所有资源还是部分资源?是 HTML 还是 API?用 RUM 数据按地区、运营商、时间看成功率和耗时分布。如果压根没有这类监控,那第一步是先补上。
分层排查。
DNS:该地区的 DNS 解析是不是慢或者解析到了错误的节点。可以用第三方拨测服务(多地区节点)看解析结果和耗时。国际业务用 GeoDNS 做地理调度,配置错误会把用户导到很远的机房。
CDN:该地区有没有覆盖节点、回源率是不是异常高(缓存没命中就要跨洋回源,耗时翻几倍)、有没有节点故障。查 CDN 厂商的地区级监控。不同 CDN 厂商的地区覆盖差别很大,某些地区可能需要换厂商或者用多 CDN 融合调度。
网络链路:跨境链路本身劣化(这个你控制不了但要能识别),或者被当地网络策略限制(某些国家会屏蔽特定域名或 IP 段)。用当地节点做 traceroute 和 MTR 看丢包和跳数。
源站:如果是 SSR 或 API,源站是否只部署在一个地区,导致远端用户每次请求都要跨洋。这属于架构问题,解法是多地区部署或者边缘渲染。
第三方依赖:这个特别容易被忽略——页面里引入的第三方脚本(统计、客服、字体、地图)如果它们的服务在某地区不可达,会阻塞你的页面加载。典型案例是引入了在某些地区访问不到的字体服务,导致整页卡住。所以第三方资源必须异步加载并且有超时降级,关键资源要自托管。
排查工具:多地区拨测(第三方服务或自建)、CDN 厂商控制台的地区数据、RUM 按地区分组、以及让当地用户配合抓取 HAR 文件(最直接的证据)。
应急和长期:应急可以切换 CDN 厂商或线路、启用备用域名、临时关掉有问题的第三方。长期要做多 CDN 容灾和自动调度、关键地区就近部署、第三方依赖自托管或降级、以及建立分地区的可用性看板和告警——国际业务里「某个国家挂了但全局指标正常」是常态,没有分地区监控就等于瞎着。
这类问题八成不是「算错了」,而是「口径不同」或「权限范围不同」,所以排查顺序要先排除这两个再看计算。
第一步先问清他拿什么对比的:他自己的表格、另一张报表、还是上个月的同一张报表。三种情况的排查方向完全不同。
第二步查权限范围,这是最高频的原因。如果他是和同事的数字对比,那两人的行级权限范围可能不同,看到的本来就是不同的数据子集——这不是 bug 是设计。正确的做法是在界面上显式展示「本报表基于你管辖的 3 个部门」,让差异可解释;而更严重的一种是汇总与明细的权限范围不一致(汇总走了无权限的预聚合、明细走有权限的实时查询),表现是「总额 100 万但下钻只有 60 万」,这是真 bug 而且是信息泄露。
第三步查口径与时间范围。指标定义变过没有?他打开的是不是几个月前保存的报表——我们踩过:调整了「活跃用户」的定义,用户打开三个月前保存的报表,看到的数字和当时不一样,认为数据出错了。所以保存的配置要记录当时的口径版本,定义变更后打开时明确提示「此报表基于旧口径」。另外查时区(跨时区客户的「今天」是哪个时区的今天)与统计截止时间(实时还是 T+1)。
第四步查聚合方式。比率类指标被求和会得出荒谬的结果(超过 100% 的转化率)。查该指标在元数据里的聚合方式是否正确——比率必须走「先聚合分子分母再相除」。
第五步才是查计算本身:拉出这次查询的实际 SQL 或查询描述,用同样条件手工核算一批明细。
长期防护:口径版本化并在历史报表上提示;指标聚合方式做成元数据而不是靠命名猜;界面显式展示数据范围与统计时间;以及提供「查看本报表的口径说明」入口——大量这类咨询其实是口径不透明造成的。
按「有没有保存成功 → 有没有发布 → 有没有被覆盖 → 缓存有没有失效 → 前台读的是哪一层」五段查,这个顺序能覆盖绝大多数情况。
第一段:确认真的保存成功了。看变更记录里有没有这一条、操作人与时间对不对。有一类很隐蔽的情况是「他改的字段被校验拦下但提示不明显」,他以为保存了。
第二段:确认发布了。很多后台是「保存草稿」与「发布」两步,运营只点了保存。或者配了定时生效而时间还没到。
第三段:查有没有被覆盖,这是我们踩过的重点。两种形态。一是自动化写入覆盖了人工修改:如果人工修正和自动同步(供应商推送、总部下发)存在同一个字段上,同步就是覆盖——运营改了、当晚同步又写回去了,他改了三次以为自己没保存成功。解法是分层:同步只写「来源方」那一层,人工修正独立成层且优先级最高。二是并发编辑互相覆盖:两个运营同时编辑,如果提交是整体提交,后提交的会把先提交的盖掉——只要一份数据有多个并发编辑者就绝不能整体提交,要只提交有变化的字段。
第四段:查缓存失效。配置类数据通常有多层缓存(服务端本地缓存、分布式缓存、CDN、客户端缓存)。逐层确认,而且要特别查「有没有反向索引」——我们踩过:只做了 TTL 过期没做事件失效,运营反馈「明明关房了列表还显示有」;补事件失效时才发现缺一个从子项到聚合对象的反向索引,事件来了不知道该失效谁。缓存失效的设计必须和缓存构建同时做。
第五段:确认前台读的是哪一层。如果前台展示走的是缓存或预聚合,而运营改的是源数据,那「没生效」是设计上的延迟窗口而不是故障——要看这个窗口是否过长,以及产品上有没有明确告知。
长期防护:界面上区分「继承/来源」与「本地覆盖」并提供恢复继承(用户看不见的继承关系等于不存在);提交一律增量;关键配置提供「生效预览」让运营输入条件反查将命中哪一条——这比让他自己推可靠得多。
没有匹配的题目,换个关键词试试。
场景题 · Vue · 共 29 题 · 设计题看方案与权衡,排查题看思路与分层