1. 被忽视的浏览器原生能力:NetworkInformation API 到底能做什么
第一次接触NetworkInformation这个 API 是在做一个移动端视频播放器的时候。产品经理提了一个需求:用户在地铁里、电梯里这种弱网环境下,视频要自动降码率,别让用户盯着转圈圈。当时团队第一反应是搞个测速接口,定时 ping 一下服务器算延迟。结果方案评审的时候,一位老前端提了一句:“浏览器不是自带网络状况查询吗?” 这才把navigator.connection翻了出来。
NetworkInformationAPI 是挂在navigator.connection上的一个浏览器原生接口,它能让网页直接读取当前设备的网络连接类型、有效带宽估算值、往返延迟估算值,以及是否开启了省流量模式。说白了,就是浏览器把底层操作系统和网络栈掌握的信息,通过一个 JavaScript 对象暴露给开发者。你不需要自己写测速逻辑,不需要额外发请求,直接读属性就能拿到一个“大概”的网络画像。
这个 API 解决的核心问题是:让 Web 应用能够感知网络环境,从而做出自适应的行为调整。比如弱网时降低图片质量、延迟加载非关键资源、切换低码率视频流、暂停后台同步任务;网络恢复时再逐步提升体验。它适合所有面向移动端、面向不稳定网络环境的 Web 应用开发者,尤其是做视频、直播、在线协作、PWA 离线应用、大文件上传下载这类场景的同学。
但这里必须先泼一盆冷水:这个 API 的兼容性和数据准确性,远没有它的名字听起来那么美好。它不是一个“精确测量工具”,更像是一个“粗略的环境感知信号”。用得好能显著提升体验,用不好反而会引入误判。接下来我会把这个 API 的里里外外拆开讲清楚,包括它的属性含义、浏览器支持现状、实际项目中的使用姿势、踩过的坑,以及一套可以直接抄作业的降级方案。
2. 核心属性逐个拆解:每个字段背后的含义与陷阱
2.1 connection 对象的基本结构
在支持该 API 的浏览器里,navigator.connection会返回一个NetworkInformation对象。这个对象上挂着一批只读属性,同时继承自EventTarget,可以监听change事件。核心属性包括:
type:当前连接类型,取值有bluetooth、cellular、ethernet、none、wifi、wimax、other、unknown。effectiveType:有效连接类型,取值有slow-2g、2g、3g、4g。这是最常用的字段。downlink:下行带宽估算值,单位是 Mbps,保留到小数点后一位或两位。rtt:往返延迟估算值,单位是毫秒,一般取最近几次请求的中位数。saveData:布尔值,表示用户是否开启了省流量模式。downlinkMax:最大下行带宽,只有部分浏览器支持,且在很多环境下返回Infinity。
这些属性都不是实时精确值,而是浏览器根据最近一段时间的网络活动估算出来的。effectiveType的判定逻辑尤其值得说道:它并不是直接读取物理网络制式,而是结合了rtt和downlink两个估算值,按照一套阈值规则映射出来的。
2.2 effectiveType 的判定阈值
根据公开的规范描述,effectiveType的映射规则大致如下:
| effectiveType | 最小 RTT | 最大下行带宽 |
|---|---|---|
| slow-2g | 2000ms | 50 Kbps |
| 2g | 1400ms | 70 Kbps |
| 3g | 270ms | 700 Kbps |
| 4g | 0ms | 不限 |
这张表非常关键。它意味着:一个连着 WiFi 但实际出口带宽很差的网络,可能被判定为 3g 甚至 2g。反过来,一个 4G 手机信号满格但基站拥塞的场景,也可能被降级判定。所以千万不要用type === 'wifi'来判断“网络一定好”,这是新手最容易犯的错误。
我在一个海外项目里就遇到过这种情况:用户连的是酒店 WiFi,type返回wifi,但effectiveType是2g,rtt高达 1800ms。如果当时只判断type,视频就会以高码率加载,结果卡到用户直接退出。后来改成以effectiveType为主、rtt为辅的判断策略,体验立刻好转。
2.3 downlink 和 rtt 的估算机制
downlink和rtt的估算基于浏览器最近观察到的一批网络请求。浏览器会记录这些请求的吞吐量和响应时间,然后取一个统计值(通常是中位数或加权平均)作为估算结果。这里有几个重要特性:
第一,它们有滞后性。如果你刚打开页面,还没有发生任何网络请求,这两个值可能是初始默认值,比如downlink为 10、rtt为 0,或者直接是undefined。所以不要在页面加载的第一时间就依赖它们做关键决策。
第二,它们会被缓存。浏览器不会每次读取都重新计算,而是按一定周期更新。不同浏览器的更新频率不一样,有的几十秒,有的几分钟。
第三,它们受请求类型影响。如果最近都是小请求(比如 API 轮询),downlink的估算可能偏低;如果刚下载了一个大文件,估算会更接近真实带宽。
提示:在实际使用中,建议把
downlink和rtt当作“参考信号”而非“决策依据”。真正做码率切换时,还是要结合播放器的实际缓冲状态和卡顿率来综合判断。
2.4 saveData:一个容易被忽略但很有价值的字段
saveData表示用户是否在系统或浏览器设置里开启了“省流量模式”。这个字段的价值在于:它代表了用户的主观意愿。用户主动开启省流量,说明他对流量消耗敏感,这时候你应该主动减少自动播放、降低预加载、压缩图片。
我在一个资讯类 PWA 项目里做过 A/B 测试:对saveData === true的用户关闭视频自动播放并降低首屏图片质量,结果这部分用户的次日留存提升了约 8 个百分点。这个字段的兼容性也还不错,主流移动浏览器基本都支持。
3. 浏览器兼容现状与降级策略设计
3.1 各浏览器支持情况
截至我最近一次系统梳理,NetworkInformation的支持情况大致是:
| 浏览器 | connection 对象 | effectiveType | downlink | rtt | saveData | change 事件 |
|---|---|---|---|---|---|---|
| 某主流 Chromium 内核浏览器 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
| 某主流移动端浏览器 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
| 某桌面端浏览器 | 部分支持 | 部分支持 | 部分支持 | 部分支持 | 不支持 | 部分支持 |
| 某另一内核浏览器 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 |
可以看到,Chromium 系和移动端支持较好,但桌面端和另一大内核的浏览器支持参差不齐。所以降级方案是必须的,不是可选的。
3.2 三层降级策略
我在项目里总结了一套三层降级策略,从强到弱依次是:
第一层,原生 API 可用:直接读navigator.connection,监听change事件,实时响应网络变化。
第二层,原生 API 不可用但可测速:在应用启动时发一个小请求(比如一个 1KB 的图片或一个轻量接口),测量响应时间,粗略判断网络质量。这个方案会增加一次请求,但能覆盖不支持原生 API 的环境。
第三层,完全不可用:使用保守的默认策略,比如默认按 3g 环境处理,图片懒加载、视频不自动播放、预加载关闭。宁可保守,也不要激进。
function getNetworkProfile() { const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection; if (conn && conn.effectiveType) { return { level: conn.effectiveType, downlink: conn.downlink, rtt: conn.rtt, saveData: conn.saveData, source: 'native' }; } return { level: 'unknown', downlink: undefined, rtt: undefined, saveData: false, source: 'fallback' }; }这段代码是基础封装,注意mozConnection和webkitConnection这两个前缀,虽然现在很多浏览器已经不需要了,但加上去能覆盖一些老版本,成本极低。
3.3 为什么不能只依赖 change 事件
change事件在网络切换时触发,比如从 WiFi 切到蜂窝、从 4G 降到 3g。但它的触发时机和频率在不同浏览器里差异很大。有的浏览器在网络类型变化时立即触发,有的则要等到下一次网络请求完成后才触发。更麻烦的是,有些浏览器在effectiveType变化时并不触发change,只在实际连接类型变化时才触发。
所以我的做法是:change 事件用来做即时响应,同时加一个定时轮询作为兜底。轮询间隔不用太短,30 秒到 60 秒一次就够了,避免不必要的性能开销。
let lastProfile = getNetworkProfile(); function handleNetworkChange() { const current = getNetworkProfile(); if (current.level !== lastProfile.level || current.saveData !== lastProfile.saveData) { lastProfile = current; applyNetworkStrategy(current); } } const conn = navigator.connection; if (conn && conn.addEventListener) { conn.addEventListener('change', handleNetworkChange); } setInterval(handleNetworkChange, 45000);4. 实战场景:把网络感知能力落到具体业务里
4.1 视频播放器的码率自适应
这是NetworkInformation最经典的应用场景。核心思路是:根据effectiveType和rtt选择一个初始码率,然后在播放过程中结合缓冲状态动态调整。
我参与过的一个点播项目里,初始码率选择逻辑是这样的:
function pickInitialBitrate(profile) { const { level, rtt, downlink } = profile; if (level === 'slow-2g' || level === '2g') { return 240; // 240p } if (level === '3g') { return rtt > 500 ? 360 : 480; } if (level === '4g') { if (downlink && downlink < 2) return 480; if (downlink && downlink < 5) return 720; return 1080; } return 480; // 未知环境走保守值 }这里的关键点是:初始码率宁可保守,也不要激进。因为起播阶段用户最敏感,一旦卡顿,用户流失率极高。后续通过播放器的缓冲水位和卡顿事件再逐步升码率,用户感知会好很多。
另外,saveData === true时,我会直接把初始码率压到最低档,并且关闭自动播放。这是对用户意愿的尊重,也是提升留存的有效手段。
4.2 图片与资源的差异化加载
在电商类页面里,图片是流量大户。根据网络状况差异化加载图片,能显著提升弱网下的首屏速度。
具体策略是:effectiveType为slow-2g或2g时,只加载缩略图,用户点击后再加载大图;3g时加载中等尺寸;4g时加载高清图。同时结合saveData,开启省流量时一律走缩略图。
实现上可以用IntersectionObserver做懒加载,在图片进入视口时再根据当前网络状况决定加载哪个 URL。这里要注意:网络状况可能在用户浏览过程中变化,所以每次加载图片前都应该重新读取一次 profile,而不是在页面初始化时读一次就固定下来。
4.3 数据同步与后台任务的调度
对于有离线能力的 PWA 应用,后台同步任务的调度也可以参考网络状况。比如:
effectiveType为4g且saveData为false时,允许后台预同步未来几天的数据。3g时只同步当前需要的核心数据。2g或更差时,暂停所有非关键同步,等网络恢复再执行。
这个策略在新闻类、笔记类应用里效果很好。用户在地铁里打开应用,不会因为后台同步把带宽占满导致页面卡死。
注意:后台同步任务一定要设置超时和重试上限。弱网环境下请求可能长时间挂起,如果不设超时,任务队列会越积越多,最终拖垮应用。
5. 常见问题与排查技巧实录
5.1 为什么 connection 对象存在但属性全是 undefined
这是最常见的问题。原因通常是:浏览器虽然暴露了navigator.connection对象,但出于隐私或实现进度原因,并没有填充具体属性。这种情况在部分桌面浏览器和隐私模式下会出现。
排查方法很简单:打印整个对象,看看有哪些自有属性。如果effectiveType是undefined,就说明这个环境不可用,直接走降级逻辑。
const conn = navigator.connection; if (conn) { console.log(Object.keys(conn)); console.log('effectiveType:', conn.effectiveType); }5.2 effectiveType 一直是 4g 不变
有同学反馈说,无论怎么切换网络,effectiveType始终是4g。这通常有几个原因:
第一,浏览器缓存了估算结果,更新周期较长。可以尝试在切换网络后主动发起一次请求,触发重新估算。
第二,某些浏览器在桌面端默认返回4g,不做实际估算。这时候需要结合rtt和downlink来判断,如果这两个值也是默认值(比如rtt为 0、downlink为 10),基本可以认定数据不可信。
第三,测试环境本身网络就很好,确实一直是 4g。这种情况可以用浏览器开发者工具的网络节流功能模拟弱网,但要注意:节流模拟不一定能改变effectiveType,因为它是基于真实网络栈的估算,不是基于开发者工具的模拟。
5.3 change 事件触发过于频繁
在某些网络不稳定的环境下,change事件可能频繁触发,导致策略反复切换,反而影响体验。解决办法是加一个防抖或节流,比如 2 秒内只响应一次变化。
let changeTimer = null; function onNetworkChange() { if (changeTimer) return; changeTimer = setTimeout(() => { changeTimer = null; handleNetworkChange(); }, 2000); }5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方向 | 解决思路 |
|---|---|---|---|
| connection 为 undefined | 浏览器不支持 | 检查浏览器类型和版本 | 走降级方案 |
| 属性全为 undefined | 对象存在但未填充 | 打印自有属性 | 走降级方案 |
| effectiveType 不变 | 估算缓存未更新 | 主动发请求触发 | 结合 rtt/downlink 判断 |
| change 触发频繁 | 网络抖动 | 加日志观察频率 | 防抖节流 |
| saveData 始终 false | 浏览器不支持 | 检查兼容性 | 不依赖该字段做关键决策 |
| 桌面端数据不准 | 桌面端实现差异 | 对比移动端表现 | 桌面端降低依赖程度 |
5.5 几个我踩过的坑
第一个坑:在 Service Worker 里读不到 connection。navigator.connection在主线程可用,但在 Service Worker 的全局作用域里,navigator对象的能力是受限的,读不到这个 API。所以网络感知逻辑要放在主线程,通过postMessage把结果传给 Service Worker。
第二个坑:SSR 环境下直接访问 navigator 会报错。服务端渲染时没有navigator对象,必须在客户端 hydration 之后再读取。我一般会封装一个isClient判断,或者把网络感知逻辑放到useEffect之类的客户端生命周期里。
第三个坑:把 downlink 当作精确带宽用。前面说过,它是估算值,而且单位是 Mbps,但实际精度很差。我曾经用downlink直接计算视频分片大小,结果在部分设备上估算值偏高,导致分片过大、加载超时。后来改成用effectiveType做粗粒度分档,问题就消失了。
6. 一套可直接复用的网络感知模块设计
6.1 模块结构设计
把前面所有经验整合起来,我通常会设计一个独立的网络感知模块,对外暴露三个能力:获取当前网络画像、订阅网络变化、根据画像执行策略。模块内部处理兼容性、降级、防抖、轮询兜底。
class NetworkAwareness { constructor(options = {}) { this.pollInterval = options.pollInterval || 45000; this.debounceDelay = options.debounceDelay || 2000; this.listeners = []; this.lastProfile = null; this.timer = null; this.debounceTimer = null; this.init(); } getProfile() { const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection; if (conn && conn.effectiveType) { return { level: conn.effectiveType, downlink: conn.downlink, rtt: conn.rtt, saveData: !!conn.saveData, source: 'native' }; } return { level: 'unknown', downlink: undefined, rtt: undefined, saveData: false, source: 'fallback' }; } isChanged(a, b) { if (!a || !b) return true; return a.level !== b.level || a.saveData !== b.saveData; } notify(profile) { this.listeners.forEach(fn => { try { fn(profile); } catch (e) { console.error('network listener error', e); } }); } check() { const current = this.getProfile(); if (this.isChanged(this.lastProfile, current)) { this.lastProfile = current; this.notify(current); } } onCheck() { if (this.debounceTimer) return; this.debounceTimer = setTimeout(() => { this.debounceTimer = null; this.check(); }, this.debounceDelay); } subscribe(fn) { this.listeners.push(fn); if (this.lastProfile) fn(this.lastProfile); return () => { this.listeners = this.listeners.filter(item => item !== fn); }; } init() { this.check(); const conn = navigator.connection; if (conn && conn.addEventListener) { conn.addEventListener('change', () => this.onCheck()); } this.timer = setInterval(() => this.check(), this.pollInterval); } destroy() { clearInterval(this.timer); clearTimeout(this.debounceTimer); this.listeners = []; } }6.2 策略层的设计思路
模块只负责“感知”,不负责“决策”。决策逻辑应该由业务层根据自身需求来实现。我一般会定义一个策略映射表,把网络等级映射到具体的业务参数。
const strategyMap = { 'slow-2g': { imageQuality: 'thumb', videoBitrate: 240, preload: false, autoPlay: false }, '2g': { imageQuality: 'thumb', videoBitrate: 240, preload: false, autoPlay: false }, '3g': { imageQuality: 'medium', videoBitrate: 480, preload: false, autoPlay: true }, '4g': { imageQuality: 'high', videoBitrate: 1080, preload: true, autoPlay: true }, 'unknown': { imageQuality: 'medium', videoBitrate: 480, preload: false, autoPlay: false } }; function resolveStrategy(profile) { const base = strategyMap[profile.level] || strategyMap['unknown']; if (profile.saveData) { return { ...base, imageQuality: 'thumb', preload: false, autoPlay: false }; } return base; }这套设计的核心思想是:感知与决策分离。感知层保证数据尽可能准确和及时,决策层根据业务特点灵活调整。这样当业务需求变化时,只需要改策略表,不用动感知逻辑。
6.3 与现有项目的集成方式
集成时我一般分三步走:
第一步,在应用启动时初始化模块,读取一次初始画像,应用到首屏渲染策略上。这一步要注意 SSR 兼容,确保在客户端才执行。
第二步,在关键业务组件里订阅网络变化,比如视频播放器、图片列表、同步管理器。每个组件根据自己的需求响应变化。
第三步,在应用销毁时调用destroy,清理定时器和监听器,避免内存泄漏。
提示:如果你的应用有多个页面或路由,建议把网络感知模块做成全局单例,避免每个页面都初始化一套,造成重复监听和资源浪费。
7. 关于这个 API 的一些个人判断
用了几年下来,我对NetworkInformation的态度是:它是一个有价值的辅助信号,但绝不是可以单独依赖的决策依据。它的最大价值在于“零成本获取一个粗略的网络画像”,让你在没有任何其他信息的情况下,能做出比“一刀切”更聪明的选择。
但它的问题也很明显:兼容性碎片化、数据精度有限、更新时机不可控。所以我的实践原则一直是:原生 API 能用就用,不能用就降级,任何关键决策都要有兜底逻辑。视频码率不能只看它,还要看缓冲状态;图片加载不能只看它,还要看视口和用户行为;后台任务不能只看它,还要看电量和用户设置。
另外,隐私方面也值得留意。这个 API 暴露的网络信息虽然不涉及具体内容,但理论上可以用于设备指纹的辅助识别。所以我在项目里从不把它和其他设备信息组合起来做用户标识,只用于当次会话内的体验优化。这既是合规考虑,也是技术伦理。
最后分享一个小技巧:如果你不确定某个环境下的数据是否可信,可以在开发阶段加一段日志,把effectiveType、rtt、downlink和实际请求的耗时、吞吐量一起打出来,对比几次就能摸清这个环境的脾气。这比看任何文档都管用。