ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

告别“JS 依赖症”:从 JavaScript 中心回到 Web 平台中心

告别“JS 依赖症”:从 JavaScript 中心回到 Web 平台中心 这几年前端圈有个挺明显的现象大家好像得了“没有 JavaScript 就不会写网页”的病。一个折叠面板、一个弹窗提示、甚至一个只在鼠标悬停时变色的按钮都要先npm install一个组件库再拉起一整套依赖链。曾经的创新精神变成了“发散式创新”我们有无数个框架、无数种状态管理方案、无数个构建工具却越来越难回答一个朴素的问题——这个页面真的需要这么多脚本吗这篇文章聊的正是这件事。我不是要劝你“反框架”或“回归 jQuery 时代”而是想以一个做过多年前端、也踩过不少重 JS 架构坑的从业者身份聊聊现代前端架构里“过度依赖 JavaScript”到底会让你失去什么以及当我们把注意力从“JS 中心”拉回“Web 平台中心”之后有哪些立得住、能落地的破局手段。内容偏工程实践适合正在维护重型单页应用、被首屏白屏和包体积困扰的团队也适合做新项目技术选型时想更理性的开发者。1. 先认识“过度依赖 JavaScript”的长相1.1 一切功能都要先问“用哪个库”我见过太多这种场景新需求提过来第一反应不是“原生 API 能不能做”而是“哪个库比较成熟”。轮播图用库、懒加载用库、表单校验用库、图片灯箱用库、甚至颜色的明暗切换都要引一个颜色处理工具库。这些需求的底层逻辑浏览器早就提供或者只需要几十行代码就能覆盖但团队已经习惯了“站在别人的轮子上”。典型例子是一只手数不完的折叠面板原生details/summary就能实现无需 JavaScript。Tab 切换原生 button 少量事件委托不需要引入一个交互组件。TooltipCSS 伪元素加定位基本能解决复杂的才需要 JavaScript 辅助。表单校验HTML5 的内置校验约束覆盖绝大多数业务校验规则。懒加载loadinglazy属性图片参数都不需要。Dark modeCSS 自定义属性 prefers-color-scheme比脚本切换 class 更稳。为什么会变成这样因为我们被声明式框架和组件库的优美 API“惯坏”了。组件库帮你兜底了浏览器兼容、边界状态、键盘交互你只需要写一行Accordion items{...}/。这种心智模型在复杂应用里确实高效但付出的代价是为了一个 3KB 逻辑的折叠面板你首先得加载可能 200KB 的脚本运行时。我在这里并不是否定组件库。很多复杂业务组件比如富文本编辑器、数据表格、拖拽看板自己徒手写成本极高使用成熟库完全合理。问题在于我们把它变成了默认选项把“引库”变成了一种条件反射而不是一次谨慎的成本评估。1.2 从“SPA 可选”到“默认 SPA”另一个典型长相是架构层面的惯性。这几年除了一些纯静态页几乎新项目默认就上单页应用全家桶组件框架、路由、状态管理、请求封装、Mock 层……哪怕这项目本质上就是一个内容站几篇文档、一个博客列表、一份产品介绍。单页应用架构本身没有错但它适合的是“应用”而不是“文档”。博客、文档、营销页、新闻站这些内容型产品的核心竞争力是“内容能不能被快速读到”而不是“页面切换需不需要整体刷新”。结果很多团队为了这个并不存在的“顺滑体验”把内容锁进了一大段 JavaScript 里用户要等 HTML 里的脚本下载完、解析完、执行完页面上才出现真正的内容。在弱网设备上这基本等于让读者等上好几秒。这里不是说“不要用 SPA”而是说架构选型应该从“产品信息架构和用户核心任务”出发而不是从“团队当前的技术栈习惯”出发。我自己经历过一个很典型的项目一个纯粹的产品文档站点用了全套 SPA没有任何复杂交互首屏却要加载 1MB 以上的脚本。后来改成静态生成首屏几乎瞬时打开SEO 问题也消失了。成本很低效果巨大。1.3 三大症状自查表如果你不确定自己的项目有没有“过度依赖 JavaScript”可以用下面这张表快速对号入座症状外在表现深层后果首屏白屏页面打开后先空白脚本执行后才渲染内容核心体验完全依赖 JS 执行速度交互延迟点击按钮后卡顿滚动掉帧大型脚本占用主线程用户感知明显SEO 不稳定搜索引擎抓不到内容或收录后内容反复变化内容曝光受损业务增长受限可访问性退步没有 JS 时页面完全无法使用ARIA 混乱键盘用户和辅助技术用户被排除构建链路臃肿打包体积持续膨胀依赖升级连环出错每次发布都提心吊胆维护成本不可控框架绑定加深业务代码与组件库深度耦合换技术栈几乎等于重写技术债逐年累积团队被锁死在旧生态里如果你满足其中两三条不用太焦虑后面有具体的破局步骤。但首先要正视一个本质问题过度依赖 JavaScript 的代价远远不止“页面慢一点”这么简单。2. 陷阱到底藏在哪四个高成本代价2.1 性能税主线程是最贵的资源很多人对 JavaScript 体积的敏感度比对图片和 CSS 低得多因为在本地开发环境浏览器解析几百 KB 脚本常常是“瞬间”的。但在真实的低端移动设备上这笔账完全是另一番光景。以一块 600KB 的压缩后脚本为例在现代应用里并不算夸张它在低端设备上的解析和编译时间可能达到几百毫秒这还只是“把代码变成可执行状态”的时间还没算执行时间。要知道 JavaScript 的下载、解析、编译、执行全部发生在主线程上而主线程同时还要处理布局、绘制、用户事件响应。页面打开后的关键交互时机就这样被一段一段的脚本税吃掉了。还有一个常被忽略的现实现在很多“首屏要求动态化”的页面真正动态的区域其实只占屏幕的 30%其余部分是静态内容。但如果你选择全站客户端渲染浏览器就不得不把整棵组件树的脚本都执行一遍。这个过程我说得直白一点你为了一个用户可能根本不会触发的交互强迫所有用户都先交一遍税。我建议每个团队都做一个简单的实验在浏览器开发者工具里把 JavaScript 禁用然后刷新你的页面。观察页面还会剩下什么。如果剩下的只有一片空白那说明你的整套架构已经把所有内容都押在了脚本上——这个押注对性能、对可访问性、对业务韧性来说都是危险的。2.2 可访问性与 SEO 的折损过度依赖 JavaScript 的第二个代价更加隐蔽因为它不会直接反映在页面速度上却会切切实实影响业务内容的可访问性。搜索引擎爬虫这些年在 JavaScript 渲染方面的能力提升了很多主流搜索引擎已经能抓取到客户端渲染后的内容。但不要忽略一个细节这种抓取的效率和稳定性远不如静态 HTML。爬虫要执行完整的脚本生命周期还要等待异步数据返回稍有不慎就会拿到半成品页面。另外对于内容型网站搜索收录是生命线把内容命运交给“爬虫愿不愿意等待脚本执行”本身就是把自己放到了被动位置。可访问性方面问题更明显。屏幕阅读器和其他辅助技术对 JavaScript 动态生成内容的理解始终不如对静态语义化 HTML 的理解来得稳定。我们常常一边写着一堆奇怪的 ARIA 属性来弥补自定义组件的语义缺陷一边却忽略了最基础的原生button、原生dialog、原生表单控件已经自带良好的键盘交互和辅助技术支持。这些话当然不是全盘否定动态渲染而是说我们应该把“内容优先、增强渐进”作为默认原则核心信息、核心操作在脚本还没执行时就应该存在脚本负责的是“升级体验”而不是“创造体验”。2.3 代码体积失控与依赖链膨胀现代应用里有一句玩笑话node_modules的重量比得上一个小型操作系统。依赖链的膨胀带来的问题首先是构建时间变长然后是运行时体积变大更麻烦的是升级的连环爆炸——你升级一个组件的次要版本结果它依赖的某个传递依赖更新了 API你的整个构建挂了。我见过一个印象很深的案例某个项目为了做一个下拉多选组件引入了一个库这个库为了支持主题定制又引入了一整套样式方案最后这个组件的体积比自己写的业务逻辑还大。这种“为了一个小功能引入一个中型框架”的操作在重 JS 架构里几乎每天都在发生。依赖链还有个被忽视的安全问题你其实不清楚依赖的依赖是谁写的、有没有被维护。一行npm install拉进来的是成百上千个包每一个都有可能成为供应链上的薄弱点。减少不必要的依赖不只是性能洁癖也是安全卫生。2.4 维护性与团队协作的熵增最后一点可能也是真正让很多团队痛苦的维护成本。当你把整个页面的状态、路由、数据获取全部交给 JavaScript 层之后调试问题就变成了“先搞懂代码逻辑”。一个按钮没反应你需要在组件树、状态管理、事件绑定、网络请求四层之间来回排查。而如果这个按钮只是一个普通的button加一段原生事件监听问题边界清晰得多。团队协作层面重 JS 架构还会迫使每个前端成员都去掌握框架生态里的各种“潜规则”生命周期顺序、状态更新时序、水合hydrate注意事项、构建插件配置。当一个团队把所有精力都花在围绕 JavaScript 的基础设施上时反而没多少人愿意去研究 HTML 语义、CSS 布局、内容结构这些“Web 基本功”。这会让团队在技术判断上越来越单一失去从平台角度思考问题的能力。3. 破局思路从“JS 中心”回到“Web 平台中心”3.1 核心原则渐进增强谈了这么多问题该聊怎么做。破局的第一步不是写代码而是切换一种心智模式从“JavaScript 先决定页面有什么”切换到“内容先去中心化JavaScript 只在边缘做增强”。这套思路有一个现成的名字渐进增强Progressive Enhancement。它有三层递进内容层核心文本、图片、链接、基础表单直接写在 HTML 里不经过脚本渲染。表现层通过 CSS 让内容呈现良好布局、层次、响应式。行为层用 JavaScript 补充增强体验比如更流畅的交互、更好的错误提示、实时校验。很多人对渐进增强的误解是“这是老古董只适用于没有复杂交互的页面”。实际上不是这样。甚至大型复杂应用也可以采用这个原则来架构应用的“外壳”可以是由服务端渲染好的静态结构复杂的交互区域单独作为增强模块加载。关键在于划分边界没有脚本时用户依然应该读到核心内容、完成核心操作只是体验可能朴素一些有了脚本体验变好而不是从不可用变成可用。3.2 让 HTML 做更多前端有一个迷你真相有些交互天生就应该让 HTML 解决JavaScript 只是给它们“伴奏”。举几个高性价比的例子details加summary一个零脚本的折叠面板天然支持键盘操作还能保留展开状态。dialog元素原生的模态框方案自带焦点陷阱和关闭事件不需要自己管理aria-hidden。原生表单校验required、min、max、pattern配合:valid和:invalid样式能做到无脚本表单交互。label与表单控件绑定扩大点击区域辅助技术也能正确识别。button而不是div能聚焦、能回车触发、能读屏识别。很多自定义组件的问题就是从一个错误的元素选择开始的。有一回我在重构一套老系统时发现“手风琴菜单”模块引了一个 60KB 的组件库。我把它替换成details再写 10 行 CSS 做过渡效果最终模块体积为 0 KBJavaScript。功能一点没少键盘可用性反而变好了。这种收益只有在亲自动手替换之后才能体会得那么直观。3.3 让 CSS 做更多CSS 这几年的进化速度快得超乎很多人预期已经完全不是那个只能管“颜色和位置”的老伙计了。现代 CSS 能承担起相当一部分原本被 JavaScript 包揽的交互和动态逻辑。几个重要能力容器查询Container Queries组件可以基于自身容器宽度做响应式而不是非得依赖视口宽度。以前需要监听 resize 然后手动切换 class 的场景现在用container就能声明式完成。:has()选择器终于能实现“根据子元素状态决定父级样式”。以前要写 JavaScript 去 toggle 父级 class现在一行 CSS 就能处理“当这个输入框有内容时让按钮变成可点击态”“当这个卡片里有图片时调整排版布局”这类关系型样式。aspect-ratio做固定比例的缩略图、视频占位不再需要 padding 百分比 hack 或脚本监听。layer和级联层从根上解决样式覆盖顺序问题不再依赖“谁后加载谁赢”这种脆弱写法。CSS Scroll Snap 和滚动驱动动画轮播图、图片画廊、吸顶导航很大一部分场景可以靠这些能力实现不再需要监听滚动位置再手动计算。千万别误以为这些能力是为了“炫技”。它们真正的价值是把页面表现层的控制权交还给 CSS从而让 JavaScript 的代码量降到“只处理真正的复杂逻辑”的程度。在性能敏感项目里CSS 的解析和渲染开销远小于等量功能的 JavaScript。3.4 用“原生组件”替代重型组件框架如果页面确实需要复杂交互组件当然要继续用 JavaScript。但这里有个思路值得推广优先考虑“原生 Web Components”而不是一上来就引入整套重型组件框架。原生自定义元素Custom Elements和 Shadow DOM 的组合能做到真正的组件封装结构、样式、行为隔离在一个自定义标签里。使用它的好处是没有运行时不依赖框架按需注册哪块需要就在哪块加载。你可以把一个小型交互组件写成原生实现然后像用 HTML 标签一样在任意地方使用。这套方案并非适合所有场景。如果你的应用是一个交互极其密集的管理后台几十个页面之间共享海量状态那现有组件框架的生态系统和性能优化确实更成熟。但如果你只有几个需要脚本的局部组件比如一个自定义下拉框、一个日期选择器、一个实时搜索框那么原生组件不仅体积更小长期维护也更省心。这里有个特别实用的技巧在拆掉重 JS 依赖的过程中你可以先用原生组件把“占整体 20% 高度交互”的页面局部包装好然后让剩余 80% 保持静态或服务端渲染。把脚本的使用范围收缩到真正在“做事情”的节点上这是最有效的一步。3.5 架构层面的减负SSR / SSG / 岛屿架构最后一个破局思路是架构层面的。既然过度依赖 JavaScript 的根源是“客户端渲染承担了所有内容展示”那对应的解法就是尽量把“展示”挪到构建期或服务端把“交互”留在客户端。具体落点有几个层次SSG静态站点生成适用内容型站点构建时生成 HTML发布后 CDN 分发。没有运行时服务器也没有客户端渲染等待首屏天然快。SSR服务端渲染个性化内容和实时数据较多的应用可以每次请求时在服务端生成 HTML交付给客户端的是一份完整的、立即可读的页面。岛屿架构Islands Architecture页面的大部分是静态 HTML只有交互区域嵌入独立的脚本片段。每个岛屿自成一体可以单独水合不需要整棵树一起激活。这比“全站水合”要理性太多。做一个简单对比你会更清楚方案内容到达用户的方式客户端脚本量级适合场景传统 CSR脚本渲染高复杂 Web 应用SSG构建期 HTML低文档、博客、营销页SSR请求期 HTML中需要实时数据的应用岛屿架构静态 HTML 局部脚本极低内容为主、局部交互的页面架构层面调整带来的收益往往是最彻底的。常见做法是翻新现有项目时先把“内容型页面”跑在 SSG 上把“工具型页面”维持 SPA 或逐渐迁到 SSR。不用一夜之间推翻所有东西选收益最大的页面先做切换就能看到明显的性能变化。4. 实操转型一份可落地的“减 JS”路线图4.1 先审计量化 JavaScript 占比转型之前先得搞清楚“钱花在哪了”。不要凭感觉猜要用数据说话。我常用的起步审计动作是打开无痕窗口在浏览器开发者工具的网络面板里找到每个请求的 JavaScript 资源把首屏所有脚本体积加起来注意看传输体积和解压体积。用 Lighthouse 或浏览器内置的性能分析工具跑几轮记录下 LCP、INP、TTI 这几个指标。用打包体积分析插件看最终的包构成找出“体积大户”到底是哪几个包。针对关键页面在代码里按模块粒度做一次“无用代码扫描”找出那些被引入但从来不会被首屏触发的模块。做完这四步你大概率会得到一个“原来如此”的瞬间首屏的 80% 脚本可能来自于登录组件、后台接口封装、第三方统计脚本而这些跟用户第一眼要看到的内容毫无关系。把审计结果整理成一个基线表指标基线值目标值后续迭代首屏 JS 传输体积记录当前值建议压到 100KB 以下每个迭代对比未使用脚本占比记录当前值尽量降为 0定期清理LCP记录当前值建议 2.5 秒以内持续观察INP记录当前值建议 200 毫秒以内持续观察无脚本核心内容可达性是否可达必须可达作为发布验收项把这些指标写进团队的性能预算里发布流水线不通过就阻断。有了预算约束代码评审时就会有动力去问“这个新依赖真的有必要吗”。4.2 从页面里剥掉不需要的 JavaScript量化之后开始动刀。我推荐的顺序是“从边缘到核心”先挑最容易下手的页面把不参与交互的组件改造成纯 HTML/CSS比如广告位、卡片列表、静态头部尾部。把首屏中“非立即需要”的交互脚本改为动态import()比如用户要滚动到评论区才加载的评论组件、用户点击“打开筛选”才加载的筛选面板。把命名事件逐个迁移到事件委托与其在每个子元素上挂监听不如在容器上统一处理同时减少事件绑定数量。全面排查第三方脚本每个第三方 JS 都会吃掉主线程时间。广告、统计、客服、埋点能合并的合并能延后加载的延后加载能剪裁参数的就剪裁。用 CSS 实现能实现的一切样式交互把 JS 里对应的“样式切换”代码删掉。比如 hover 效果、折叠动画、滚动提示。这个阶段有一个特别实用的审视标准**实现这个功能需要“监听状态变化”还是只需要“响应一个视觉条件”**前者需要 JavaScript后者大概率可以交给 CSS 选择器。拿它来判断每个需求思路立刻清爽很多。4.3 水合与岛屿选型按“交互密度”做决定做了边缘清理之后剩下的 JavaScript 会集中在真正需要交互的区域。这时候要决定的是这些交互区域用什么样的水合策略。对于一个内容型页面最常见的选择是“岛屿架构”。具体操作可以这样拆页面初始 HTML 由 SSG/SSR 生成所有内容和基础布局都不依赖脚本。找出页面上真正需要 JavaScript 的组件把它们标记为“岛屿”比如搜索框、图表、评论区。每个岛屿组件自身负责自己的客户端逻辑渲染时机由组件生命管理。页面加载时外部只需要一个极小的加载器动态加载岛屿代码。其余静态内容完全不做水合不生成任何额外脚本。如果页面里只有一个或少数几个岛屿甚至可以直接把相关脚本内联在页面里省去一次网络往返和模块加载开销。别小看这种“土办法”对单岛屿页面来说它往往比任何框架的水合机制都更快。如果整个应用交互密度极高比如后台管理系统的编辑区那么继续保持 SPA 架构是合理的选择。破局不是要求所有页面都回归 MPA而是要求每个页面都按自己的真实交互密度选择架构。4.4 度量回归用性能预算守住成果改造完成后如果不加约束代码会慢慢膨胀回去。所以“减 JS”应该被视为一项持续工程而不是一次“清理月”活动。建议团队做三件事在 CI 里加入预算检查超过 JS 体积阈值就报错阻止合并。把 LCP、INP 写进核心看板每次发布后自动归因到具体变更。在代码评审模板里增加一项这个需求新增了多少字节的 JavaScript这个依赖是必需的吗是否可以用原生能力替代有一句话我一直记着性能问题不是“写代码时想一下”的问题而是“每行代码进入主干”的问题。预算机制的意义就是确保每个成员在每次变更时都保持对这种取舍的感知。5. 踩坑实录与常见问题速查表5.1 我在改造中踩过的几个坑这个部分我掏点实际经验。改造“过度依赖 JS”的架构并不是流水线式的轻松减负自己真实做过之后遇到过不少意料之外的状况。第一个坑组件库替换成纯 CSS 后键盘交互被遗忘。我当时把一个折叠菜单改成details视觉上没问题但团队同事发现自定义图标在键盘聚焦时没有焦点样式因为原来组件库会自动处理focus-visible。后来补了几行 CSS才让键盘用户能感知焦点位置。这件事提醒我把 JS 拿掉之后配套的无障碍细节还要再补一轮。第二个坑dialog元素在旧浏览器的表现参差。原生对话框很好用但某些浏览器里的样式基础差异很大直接裸奔会导致弹窗位置错乱。解决办法是写一个极简的渐进增强样式先确保默认情况可用然后在支持浏览器里做更精细的定位。不要把新 API 当成万能药该写supports就写。第三个坑改造过程中“功能回退”现象。有些交互之前虽然用 JS 实现但用户早已形成习惯。比如原来点击某个卡片会展开动画改成纯 CSS 之后动画曲线和时序不一致产品找到我说“体验变了”。后来我把 CSS 过渡参数尽量调到与原实现一致才算平滑过渡。这个坑的核心教训是减脚本不意味着减体验视觉和交互反馈的一致性仍然要维护。第四个坑静态化之后动态内容没地方放了。一个内容站改成 SSG却忘了有些区块其实来自接口数据比如推荐列表。第一次上线后这些区块全空。我们最后采取的办法是核心页面先静态化动态区块用“客户端异步增强”的方式拉取数据后再渲染同时给未加载状态准备骨架屏。这本质上就是对“静态外壳 动态岛屿”的第一次实践。5.2 常见问题排查速查表症状可能原因破局方案首屏一直白屏首屏 JS 执行太久或依赖同步加载静态化/SSG 首屏动态import非关键代码点击按钮无响应脚本执行失败、事件绑定时机不对用事件委托绑定到容器层脚本放文档底部或加deferSEO 内容抓不到内容由客户端渲染生成改 SSR/SSG确保 HTML 里直接有文本内容移动端滚动卡顿滚动画监听较多或动画占满主线程用 CSS 动画、scroll-snap、IntersectionObserver 替代手动计算可访问性不达标自定义组件缺少原生语义优先使用原生元素ARIA 作为最后补充包体积持续膨胀依赖链导致高耦合建立体积预算定期做依赖分析砍掉重复封装这张表可以贴到团队文档里遇到类似问题先查表再排查代码。它不能解决所有场景但能帮团队从“重 JS 惯性”里跳出来先思考平台本身是否已经提供了方案。5.3 破局过程中的心态与协作建议架构改造最难的往往不是技术而是人的惯性。团队里一定会有人说“我们原来的方式也没出什么大问题”也会有人担心“把脚本减掉是不是会让功能变弱”。我自己的建议是不要一开始就喊“我们要全面去 JS 化”那个目标不仅激进而且也不合理。更可执行的做法是找一个肉眼可见收益的场景比如首屏性能很差的落地页或文档页先做一次减 JS 改造。把改造前后的性能数据首屏体积、LCP、INP贴出来让团队看到真实的差距。制定一个“预算红线”把“新增脚本需要说明原因”写进协作规范。把本次改造中使用的原生 API、CSS 能力、架构模式整理成一份团队内部文档方便新人快速上手。同时也要给团队留退路。如果某个场景证实了原生方案在复杂环境下确实力不从心那就大方回归组件框架。这套方法的真正目的从来不是“消灭 JavaScript”而是让每一种技术都待在它该在的位置。最后再分享一个小技巧把页面在浏览器里禁用 JavaScript 跑一遍你立刻就知道现有脚本到底是在为“用户需求”工作还是在为“开发者的习惯”工作。我第一次跑这个实验的时候看到自己的页面只剩一片空白说实话挺震撼的。后来每一轮改造我都拿这个实验当验收关卡内容还在核心操作还能完成才算真正迈过了“过度依赖”的门槛。我个人在实际操作中的体会是这类改造的收益周期很短通常一两周内就能看到首屏性能和包体积的明显改善而它的长期价值在于让团队重新回到“Web 平台本位”的思路——写更少的代码做更可靠的产品。如果你也被重前端架构的复杂度困扰建议从最小页面开始把 JavaScript 从主角调回它应有的位置一个随叫随到的增强器而不是整个架构的承重墙。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进