如果你今天打开一个内容型网站的首页把浏览器切到“禁用 JavaScript”的状态再用手机流量这种弱网环境访问一次多半会看到一个半死不活的白屏或者一个转了几秒还在打转的 loading 动画。很多人已经默认这就是现代 Web 的宿命觉得“没有 JS 的 Web 没意义”。可恰恰是这个默认观念正在让太多前端架构往同一个偏方向上狂奔。行业里判断一个前端工程师的水平常常看他能把多少复杂的交互做得流畅。但很少人反过来问这个页面里有多少交互是用户真正需要的有多少状态管理是为了管理一个本来可以由服务器输出的页面而存在的有多少副作用是框架强加给这个项目的当一个团队在“发散创新”的旗帜下各自发挥——有人用组件化框架写页面有人单独为一个小按钮引入交互方案有人搭建一套完整的私有状态层——页面就在这些局部创新里越滚越大最终 JavaScript 不再是“增强体验的脚本”而是架构本身。这篇文章想聊的不是“消灭 JavaScript”也不是给某一种框架定生死。我想拆开“过度依赖 JavaScript”这件事的完整账本再聊几条可以落地的破局路线。如果你正被首屏性能、构建速度、团队协作成本折磨过也许这里面的某些思路能用得上。1. 先承认一件事JavaScript 依赖不是“用得多”而是“越了界”1.1 从增强脚本到宿主应用一次失控的渐变早期的 Web 里JavaScript 的位置其实很边缘表单校验、图片滚动、弹窗提示都是些“锦上添花”的事。页面完整内容先由服务器输出脚本只是在 DOMContentLoaded 之后动一动某些节点。后来组件化框架改变的不仅是写法。它们把整个页面挂在一个div idapp上让“页面内容”变成了 JavaScript 运行时的产物。这个变化本身没有对错但它挪动了一个关键边界内容的渲染职责从服务器转到了浏览器脚本。于是出现了很多在今天看很常见的组合一个只有三千字介绍文案的营销页加载了大约 600KB 的 JavaScript 跑到客户端拼页面一个面向内部员工的信息查询列表为了几个筛选项和分页功能搭建了一个拥有完整 Router、状态层、请求层、UI 组件库的“小型操作系统”。这些工程体量当然有它们存在的理由但更多时候理由并不充分。1.2 发散创新是怎么成为债务加速器的“发散创新”不是贬义词。它指的是团队在既定方案之外探索局部最优解的冲动为了让某张图表更快有人绕过组件库手写操作为了让某个详情页秒开有人把接口数据预编译进了静态文件为了让中后台表单更好用有人引入了第二套状态管理方案。如果每一条创新最后都能收敛回统一的架构约定里那这些分散的尝试就是有价值的实验。但实际项目中发散创新往往缺一个收敛步骤。每个小组的局部最优解叠加起来反而形成了一张越来越臃肿的依赖图。我在很多项目里都能看到类似的痕迹某个包全项目只剩两个组件在用但因为迁移成本高谁都不敢删某个全局状态库只服务于一个页面的一个弹窗但它被放进每个模块的初始化流程里。这让我越来越相信前端架构最大的风险不是技术选型错误而是缺少一个“这个功能到底值不值得用 JavaScript 实现”的判断机制。1.3 重新划定职责边界要判断“越界”可以做一个简单的职责划分内容渲染、路由导航、表单交互、样式呈现、状态保持哪些是本该由服务器输出的哪些是浏览器原生能力可以做到的哪些是脚本介入后收益远大于成本的我自己的经验是先把问题倒过来列一份《必须使用 JavaScript 清单》需要实时双向同步的复杂交互比如协作编辑需要高频更新的可视化视图需要客户端持久化或者离线能力的场景需要访问设备 API 的场景需要前端做复杂校验和中间态管理的表单剩下的东西包括文章内容、列表结构、首屏样式、基本导航、详情展示几乎都可以靠服务端输出配合 CSS 搞定。把这条线画清楚之后“过度依赖”的程度立刻就能暴露出来。2. 过度依赖 JavaScript 的七本账每一笔都会在某个时间点结清2.1 首屏性能税最沉重的隐形开销首屏渲染是 JavaScript 依赖最直接的成本。用户打开页面的那一瞬间需要先下载脚本再解析、编译、执行之后才能把界面画出来。在 4G 到 5G 切换的移动网络环境里这一段延迟是看得见摸得着的尤其在低端安卓机上运行时的解析执行时间往往比下载时间更致命。有一个直观的类比如果页面内容是一段写在 HTML 里的文字那它就像直邮到户的信件打开信封就能看到如果页面内容必须通过 JavaScript 渲染那它就像需要你先组装一台打印机再由打印机把信打出来。组建打印机本身没有错但如果只是为了看一行文字那这笔开销明显不合理。解决方向也不是一刀切。对内容密集型页面优先让服务器把 HTML 发出来对真正复杂的应用则要把加载过程拆成“必要加载”和“按需加载”两部分让核心路径上的脚本量尽量小。2.2 可靠性折损一个异常就能带走整屏内容当整个页面都由 JavaScript 产出时可靠性就绑在了脚本执行是否成功上。无论是一个未被捕获的运行时异常还是某个接口返回了无法处理的数据结构结果都可能是白屏、空白区块、无限 loading或者状态错乱。脚本异常的范围比想象中大。第三方 SDK 报错、浏览器升级后某个 API 变更、用户安装了某种屏蔽脚本的扩展、公司内部监控脚本抢占了全局变量……这些都是极常见的事。原生 HTML 在这类问题上要可靠得多因为浏览器负责把内容画出来脚本只是附加动作。2.3 SEO 与内容可达性爬虫不买账搜索引擎爬虫和各类内容抓取工具在 JavaScript 执行上一直持保守态度。就算现代搜索引擎对客户端渲染的容忍度提高了“能抓”和“抓得好”也不是一回事。内容型网站如果把整份文章正文都交给前端脚本渲染页面收录质量、索引速度都可能出现明显波动。即使你不在乎搜索流量内容可达性仍然重要。很多企业内部的文档平台、数据分析平台分享链接时希望别人打开就能看到概要但对方环境里可能禁用脚本或者在公司内网安全策略下脚本无法完全执行。服务端直接输出内容的页面在这类场景里就是天然更强。2.4 可访问性裂缝被忽略的用户群屏幕阅读器和键盘导航对动态渲染内容的支持远比直接存在于 HTML 中的内容要薄弱。当页面结构靠 JavaScript 在运行时创建辅助技术需要等待脚本执行完成还要依赖框架生成正确的可访问性语义ARIA 属性、焦点管理、语义标签。说得直白一点用原生button和一个可访问性语义缺失的div rolebutton对比后者的键盘聚焦、朗读、表单提交行为都更难做得正确。过度依赖框架的组件抽象很容易让这些基础语义在层层封装中被稀释。2.5 维护成本依赖树不是树是热带雨林依赖膨胀带来的维护压力通常不会出现在项目初期而出现在一年之后。某个 UI 组件库升级主版本脚手架构建报错 200 条某个工具包被两个模块间接依赖版本冲突让测试环境行为诡异甚至只是删一行全局配置都有可能牵连一系列不知道从哪里延伸出来的副作用。传统多页应用时代页面之间是松耦合的一个页面的依赖故障不会波及其他页面。而在大型单页应用里全局运行时把所有模块捆在一起任何局部问题都可能被放大成架构问题。2.6 团队认知负荷上下文切换正在吃掉效率现代前端团队的开发过程往往不是围绕业务逻辑展开而是围绕框架机制展开。状态更新的触发规则、依赖收集的时机、组件生命周期的执行顺序、路由懒加载的边界……每一个都是需要额外学习的“隐式规范”。老团队成员占优势但新人上手成本显著提高。一个页面出现展示异常调试链路可能是查数据源 → 查状态更新 → 查组件重渲染时机 → 查样式优先级绕了一大圈才发现只是 CSS 里少了条规则。这种上下文切换对研发效率的损耗在项目复杂到一定程度后会变得非常明显。2.7 架构刚性框架模式反过来限制了优化组件化框架带来的编程模型本身有极强的约定性。它适合大多数场景但也让团队很难在局部做极端优化。比如某个只读页面根本不关心状态变化但为了统一初始化流程它也会被套进同一套响应式体系中某个静态区块完全不需要水合但为了和其他页面保持同构它还是会被打包进脚本。这种刚性不一定是框架的问题而是团队把自己的架构完全锁死在框架推荐模式里之后自然丧失了对局部的弹性控制。3. 破局的第一性原理HTML 与 CSS 本来就是一整套被低估的运行时3.1 把 HTML/CSS 当运行时而不是“模板”一提到“减少 JavaScript”很多人的第一反应是那交互怎么做但其实近几代浏览器原生的能力已经大幅扩张很多过去必须写脚本的交互现在原生就能完成。折叠面板可以用details和summary弹窗可以用原生dialog浮层提示可以用popover属性表单校验可以用原生required、pattern、:invalid伪类选项卡切换、响应式布局、自适应字号都有对应的 CSS 方案。更别说 CSS Grid、Flexbox、容器查询、:has()这些能力让过去需要 JavaScript 来计算位置、判断状态、动态切换类名的场景大幅减少。这背后的价值不止是省代码。原生能力是浏览器内置的行为一致性最强可访问性也有保障。你要做的只是把这些能力以合理的方式组合进页面。3.2 一个“快速筛选器”帮团队判断到底要不要 JS我在团队里用过一套很简单的筛选方法帮助大家判断一个功能到底要不要引入脚本这个功能在页面初次加载时必须立即出现吗它需要和服务端数据实时同步吗它必须依赖用户输入或设备状态吗它的视觉效果能否完全由 CSS 状态切换完成如果四个问题都是否定的那它就很可能不需要 JavaScript。举个例子一个产品展示页的轮播图如果只是每隔几秒自动换一批图片那完全可以用纯 CSS 的滚动吸附和动画实现不需要为一个轮播组件挂载一整套交互状态。3.3 “渐进增强”不是老古董而是性能预算的前置渐进增强的思路放到今天反而更清晰先把内容以最基础、最可靠的形式交给用户再根据浏览器能力逐层升级体验。具体到工程实现就是让服务端输出的 HTML 保持完整可用然后再通过脚本给页面“加点料”。这样的页面即使在弱网、脚本阻塞、用户禁用了 JS 的环境下核心内容依然可用而在现代浏览器里脚本加载完毕后会无缝地注入更丰富的交互。这么做看起来和现代框架的默认路径是反的但它带来一个非常务实的好处你把性能和可用的底线锚定在了一个极低的位置上。后续所有优化都只是在这个底线上做加法而不是在一条脆弱的主干上修修补补。4. 四条值得认真考虑的破局路线SSR 边界、岛屿架构、Web Components 与标准优先4.1 路线一把渲染主权还给服务器服务端渲染SSR不是新鲜概念但很多项目里的 SSR 其实走歪了它在服务端生成 HTML 之后依然会在客户端把整个组件树再跑一遍水合过程加载的体积并不比纯客户端渲染小多少。更健康的方式是“部分 SSR”或者说“只在必要的地方水合”。把不需要交互的页面区块直接作为静态 HTML 输出不参与客户端水合只有少数需要响应式状态的区块才生成对应的交互脚本。这样首屏脚本体积可以明显下降也不会牺牲核心交互体验。4.2 路线二岛屿架构“岛屿架构”可以看作部分 SSR 的进一步抽象。整个页面被看成一个静态 HTML 的海洋海洋里分布着一座座独立的交互岛。每一座岛都有自己独立加载的脚本岛与岛之间互不依赖。这种架构对内容型页面特别合适。比如一个长文章页文章正文是静态 HTML评论区是一套交互组件顶部工具栏又是一套交互组件。两套组件各自加载自己的脚本互不阻塞哪怕评论区的脚本挂了文章照样能读。实现思路也很直接服务端负责输出静态骨架每个动态区块用自定义元素或者独立脚本来增强自身行为。4.3 路线三原生自定义元素替代运行时Web Components 的标准能力容易被忽视但它其实很出色自定义元素可以用原生类实现影子 DOM 提供了样式隔离自定义事件可以跨组件通信不用引入任何运行时。如果团队对框架没有硬性统一要求完全可以把公共组件库改造成自定义元素实现。它的跨框架复用能力很强同一个组件可以同时被不同技术栈的页面引用不会出现“为了用这个组件必须把整个框架引进来”的尴尬。4.4 路线四先标准后增强这不算具体技术路线而是一种工程纪律实现任何一个功能前先查一遍浏览器原生能力原生方案能覆盖就直接用覆盖不了再考虑轻量脚本脚本复杂度再失控才考虑引入重型框架或状态层。这条路线的收益不是某个具体指标而是帮团队形成了一种肌肉记忆。时间久了你会越来越少地“因为框架存在所以用框架实现”而是越来越倾向于“因为确实需要才引入额外依赖”。4.5 选型参考比较破局路线适用场景JS 用量降幅交互能力实施成本服务端渲染内容站、营销页、文档站高中中岛屿架构长文章加局部复杂交互中高中高中高Web Components跨框架组件库、中后台中低高高标准优先几乎所有前端项目视场景而定中低我这几年做选型越来越不喜欢“一步到位”的决定。更靠谱的做法是先让团队把“标准优先”作为默认策略遇到具体场景再评估是否需要上 SSR 或者岛屿架构。5. 一次真实项目瘦身全过程从“全是 JS”到“JS 只在需要的地方出现”5.1 项目背景与问题定位我接手过一个内部管理系统主要功能是给业务人员展示投放数据并提供基础筛选。系统启动时构建产物接近 3MB首屏完全渲染在中等配置的开发机上大概要 4 秒左右。生产环境更差经常出现白屏报错原因基本都集中在某个第三方组件异常。当时团队的第一反应是“压代码”拆包、开启 Gzip、用流式加载效果确实有但改动一轮之后性能预算依然不够理想。真正的转折点来自一次“功能与脚本量清单”盘点我们把系统里每个页面的功能列出来然后问一个问题——如果现在完全不写 JavaScript这些功能哪些会消失结果发现真正依赖脚本的只有筛选联动、表格动态排序、详情弹层和一段图表渲染。其余绝大部分页面结构、导航、静态信息展示、表单基础布局竟然靠原生 HTML 和 CSS 就能实现。5.2 先做出《必须保留脚本》的清单这是我们第一份产出直接决定了重构范围筛选联动需要实时请求接口并更新表格必须脚本。表格排序前端排序和后端排序都可以如果切到后端排序前端脚本可以删掉一大半。详情弹层用原生dialog实现只需要一个很小的脚本封装不需要大状态层。图表渲染需要脚本但可以单独拆包不进入首屏主路径。其余所有列表页、详情页、配置页统一改为服务端直接输出完整 HTML。这么做的好处立竿见影每个页面的首屏不再依赖“应用启动”而是直接展示内容。5.3 按步骤替换不要一次推翻我们没有直接删掉框架重写而是分成了四步第一步把几个纯展示页面从组件化渲染改成服务端模板输出。这些页面原本就没有太强交互改造成本低但收益最明显。第二步把弹层、下拉筛选这类轻交互替换成原生元素和少量脚本。团队里专门排了一个同事负责把零散的 UI 组件库依赖收敛掉。第三步把表格排序切换到后端排序。前端只保留一个“点击表头触发请求”的事件绑定几十行代码完成省掉了前端对整张表的数据再处理。第四步把图表、审计日志这类重模块拆成异步加载的独立 chunk主页面完全不再引用它们。重构后的首屏脚本量从约 900KB 降到约 180KB主要来自一部分残留组件和图表逻辑。首屏时间从 4 秒左右降到 1.2 秒上下白屏报错也大幅减少。5.4 一个容易翻车的细节状态同步重构过程中踩过最典型的坑是服务端输出的 HTML 和前端脚本各自维护一份状态。举例来说筛选区选中了某个条件后重新渲染的表格如果由前端脚本生成那表格里“当前筛选条件”的展示就会和服务端 HTML 里“当前筛选条件”的展示不一致。后来我们定了一条规则页面状态优先放在 URL 查询参数里。服务端读取 URL 参数输出对应结果脚本只负责替换 URL 参数并触发页面整体刷新。这回到了最传统的“服务端渲染 URL 状态”模型看起来不够“现代”但在当前场景下它比在内存里维护两份状态可靠得多。这次重构给我最大的启发不是“技术路线要选对”而是决策顺序要反过来先搞清楚功能边界再决定用多少脚本最后才选具体实现方案。把顺序反过来几乎总会陷入“先有工具再为工具找用例”的怪圈。6. 破局落地时真正难的四个边界问题6.1 新旧架构共存不要停下来重写很多团队听到“降低 JavaScript 依赖”第一反应是找一个大项目做整体迁移。但这恰恰是风险最大的动作。更稳妥的做法是把新页面当作“标准优先”的试验田同时把存量页面的高频问题挑出来单独优化。比如哪个页面首屏最慢就先把它的静态部分抽出来做服务端输出哪个页面状态层最复杂就先去检查哪些状态其实可以外置到 URL 参数。新旧架构长期共存不是临时的妥协反而是一种有意的策略它能让你在每一步都可以回滚不被一次性大重构绑架。6.2 用性能预算守住底线所谓性能预算就是给每个页面立一个“脚本量上限”。比如定义一个指标首屏页面在手机上加载的 JavaScript 总字节数不能超过某个阈值一旦超过构建流程直接报警。这个预算机制比口头约定有用得多。它让“减少 JavaScript 依赖”不再是一种抽象理念而是每个合并请求里可以被客观检验的数字。数据不达标代码就进不了主干。6.3 团队的思维升维与技术基建过度依赖 JavaScript 从某种意义上也是团队技能的“舒适区陷阱”。大多数前端工程师的训练路径都是从框架入场很少有人系统地学习过浏览器原生能力、CSS 新特性、服务端渲染的取舍。要让团队形成新的判断力得靠两件事一是定期分享原生能力的最新进展二是建立公共组件库把“标准优先”的成果沉淀成复用资产而不是让每个工程师都从头摸索。6.4 避免从一个极端掉进另一个极端降 JavaScript 依赖不等于逢 JS 必反。如果一个功能场景确实需要复杂前端交互那它就是应该用脚本如果只是为了让“代码更少”而强行用服务端模板硬造一个体验很差的交互那属于南辕北辙。我总结成一句话降低过度依赖是为了让 JavaScript 回归“增强”的本位而不是把整个体系带进新的苦修主义里。我在自己的项目里复盘时发现真正扭转局面的往往不是某个具体框架或工具而是一套贯穿所有决策的“成本感知”。什么时候该让服务端多干一点什么时候该让脚本介入判断依据始终是同一个这一步到底给用户创造了多少可感知的价值又让架构背负了多少新的复杂度。把这个感知建起来JavaScript 使用量会自然而然回到它该在的位置上。
阅读完成 · 觉得有帮助?