1. 被忽视的浏览器内置网络探针
做前端这些年,我见过太多项目在“网络状态判断”这件事上反复造轮子。最常见的做法是监听window.online和window.offline事件,配合navigator.onLine属性来判断用户是否断网。这套方案能用,但它的粒度粗得让人难受——它只能告诉你“有网”或“没网”,至于网络是快是慢、是 Wi-Fi 还是蜂窝数据、用户是不是在省流量模式,它一概不知。
NetworkInformationAPI 就是在这个背景下进入我视野的。它是浏览器原生提供的一套网络状况查询接口,挂在navigator.connection对象上(部分浏览器也支持navigator.mozConnection和navigator.webkitConnection这两个带前缀的别名)。通过它,前端代码可以直接读取当前网络的有效连接类型、预估带宽、往返延迟,以及用户是否开启了数据节省模式。这些信息对于做自适应加载、弱网降级、流量友好型交互来说,价值非常大。
这篇文章适合谁看?如果你正在做移动端 H5、跨平台 Web 应用、视频/图片密集型站点,或者你只是单纯想搞清楚“浏览器到底能感知多少网络信息”,那这篇内容应该能给你一些可以直接落地的思路。我会从 API 的能力边界讲起,然后拆解每个属性的真实含义和坑点,再给出几套我在实际项目里用过的降级策略和封装方案。全程不堆砌概念,只讲能跑起来的代码和踩过的坑。
需要先说明一点:NetworkInformation的浏览器支持情况并不均匀,Chromium 系支持最完整,Firefox 部分支持,Safari 长期缺席。所以这篇文章的重点不只是“怎么用”,更是“怎么在支持不一致的情况下用得稳”。
2. navigator.connection 到底暴露了哪些信息
2.1 五个核心属性逐个拆解
navigator.connection对象上真正有实用价值的属性并不多,我把它归纳成下面这张表,方便你快速建立整体认知:
| 属性名 | 类型 | 含义 | 典型取值 |
|---|---|---|---|
type | string | 当前连接的技术类型 | wifi、cellular、ethernet、bluetooth、none、unknown |
effectiveType | string | 综合估算的有效连接等级 | slow-2g、2g、3g、4g |
downlink | number | 预估下行带宽,单位 Mbps | 如10、1.5、0.3 |
rtt | number | 预估往返延迟,单位毫秒 | 如50、300、2000 |
saveData | boolean | 用户是否开启了数据节省模式 | true/false |
这里有个特别容易混淆的点:type和effectiveType不是一回事。type描述的是物理链路技术,比如你连的是 Wi-Fi 还是蜂窝;而effectiveType是浏览器根据实测的带宽和延迟动态估算出来的一个“体感等级”。举个例子,你连着一个信号极差的 Wi-Fi,type会告诉你wifi,但effectiveType很可能给你标成2g甚至slow-2g。做降级判断时,effectiveType往往比type更有参考价值。
2.2 effectiveType 的估算逻辑与刷新时机
effectiveType不是一次性算完就固定不变的。浏览器会在页面生命周期内持续采样,当网络状况发生明显变化时更新这个值。它的估算大致基于最近一段时间内实际观测到的吞吐量和 RTT,然后映射到四个档位:
slow-2g:RTT 大约在 2000ms 以上,下行带宽极低,基本等同于“几乎不可用”2g:RTT 在 1400ms 左右,带宽很低3g:RTT 在 270ms 左右,带宽中等4g:RTT 在 100ms 以内,带宽较高
这些阈值是浏览器内部的经验值,不同内核可能有细微差异,但整体量级是一致的。我在实测中发现一个现象:页面刚加载时effectiveType可能还是4g,等加载了几个大资源之后,如果实际速度跟不上,它会自动降到3g。这说明它是动态修正的,而不是静态标签。
注意:
effectiveType的更新不是实时的,浏览器会做节流。你在代码里连续读取它,短时间内拿到的可能是同一个值。真正要感知变化,得靠change事件。
2.3 change 事件:网络变化的唯一可靠信号
navigator.connection上会派发change事件,这是监听网络状况变化的标准方式。写法很直接:
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; if (connection) { connection.addEventListener('change', () => { console.log('网络类型:', connection.type); console.log('有效类型:', connection.effectiveType); console.log('下行带宽:', connection.downlink, 'Mbps'); console.log('往返延迟:', connection.rtt, 'ms'); console.log('省流量模式:', connection.saveData); }); }这里有个坑我必须提醒:change事件的触发频率完全由浏览器决定,不同内核差异很大。Chromium 在effectiveType变化时会触发,但downlink和rtt的微小波动不一定触发。所以不要指望这个事件能给你毫秒级的网络监控,它更像是一个“档位切换通知”。如果你的业务需要更细的实时感知,得自己在业务层做采样,比如记录接口请求的耗时,用实际数据来辅助判断。
3. 用这套 API 解决三个真实场景问题
3.1 场景一:弱网下的图片降级加载
图片是流量大户,也是弱网体验的重灾区。传统的做法是全部加载高清图,然后靠loading="lazy"延迟加载,但延迟加载解决的是“什么时候加载”,解决不了“加载多大的图”。结合effectiveType,我们可以做到按网络等级选择不同分辨率的图源。
思路是这样的:在图片真正进入视口之前,先读取当前effectiveType,然后从srcset里挑一个合适的候选。下面是我在一个内容型站点里用过的简化实现:
function pickImageSource(imgEl) { const connection = navigator.connection; const effectiveType = connection ? connection.effectiveType : '4g'; const saveData = connection ? connection.saveData : false; const srcset = imgEl.dataset.srcset; if (!srcset) return imgEl.dataset.src; const candidates = srcset.split(',').map(item => { const [url, descriptor] = item.trim().split(/\s+/); const width = parseInt(descriptor.replace('w', ''), 10); return { url, width }; }); // 省流量模式或弱网,直接选最小图 if (saveData || effectiveType === 'slow-2g' || effectiveType === '2g') { return candidates.sort((a, b) => a.width - b.width)[0].url; } // 3g 选中等,4g 选最大 if (effectiveType === '3g') { const sorted = candidates.sort((a, b) => a.width - b.width); return sorted[Math.floor(sorted.length / 2)].url; } return candidates.sort((a, b) => b.width - a.width)[0].url; }这段代码的核心逻辑是:把网络等级映射到图片尺寸档位。实测下来,在 3g 环境下把图片从 1200px 降到 800px,首屏加载时间能缩短 30% 以上,而视觉上的差异在手机屏幕上几乎看不出来。这里的关键经验是:不要追求“精确匹配”,而是做“保守降级”——宁可稍微小一点,也不要让用户等太久。
3.2 场景二:省流量模式下的自动行为调整
saveData这个属性我觉得被严重低估了。它反映的是用户在系统层面主动开启的“数据节省”意愿,这是一个非常明确的信号:用户不想浪费流量。当saveData为true时,我们可以做一系列自动调整:
- 视频默认不自动播放,改为显示封面图加播放按钮
- 图片使用更低分辨率,或者用占位色块代替
- 关闭后台预加载、预取等“提前消耗流量”的行为
- 减少轮询接口的频率,降低不必要的请求
我在一个视频列表页做过对比测试:开启saveData检测后,省流量模式用户的平均流量消耗下降了约 45%,而页面核心功能的可用性没有受影响。这个收益是很可观的。
提示:
saveData在部分浏览器里可能始终返回false,即使系统开了省流量。所以它只能作为“加分项”来用,不能作为唯一判断依据。稳妥的做法是把它和effectiveType结合起来看。
3.3 场景三:基于 RTT 的请求超时动态调整
固定超时时间是个很粗暴的设计。在 4g 下设置 3 秒超时没问题,但在 2g 下,3 秒可能连 TCP 握手都完不成,结果就是大量本可以成功的请求被误判为失败。用rtt来动态计算超时时间,是一个很实用的优化。
我的做法是:以rtt为基准,乘以一个系数,再加上一个基础值。比如:
function getTimeout() { const connection = navigator.connection; const baseRtt = connection && connection.rtt ? connection.rtt : 100; // 基础 2 秒,加上 4 倍 RTT,上限 15 秒 const timeout = Math.min(2000 + baseRtt * 4, 15000); return timeout; }这个公式不是拍脑袋来的。rtt是往返延迟,一次完整的请求至少包含一次往返,再加上服务端处理时间和数据传输时间,所以用 4 倍 RTT 作为余量是比较合理的。上限 15 秒是为了防止极端情况下用户等太久。实测在弱网环境下,这套动态超时把请求成功率从 70% 左右提升到了 90% 以上。
4. 兼容性现实与渐进增强封装
4.1 各浏览器支持现状与检测方法
先说结论:截至我写这篇内容时,NetworkInformation的支持情况大致如下:
| 浏览器 | type | effectiveType | downlink | rtt | saveData | change 事件 |
|---|---|---|---|---|---|---|
| Chromium 系 | 支持 | 支持 | 支持 | 支持 | 支持 | 支持 |
| Firefox | 部分 | 支持 | 支持 | 支持 | 不支持 | 支持 |
| Safari | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 |
Safari 的缺席是最大的痛点,尤其是 iOS 端。所以任何依赖这套 API 的功能,都必须有完整的降级路径。检测方式很简单:
const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; const hasNetworkInfo = !!(connection && connection.effectiveType);如果hasNetworkInfo为false,就退回到默认策略——按 4g 处理,或者用实际请求耗时来做运行时判断。
4.2 封装一个可降级的网络状态模块
直接在每个业务点里写navigator.connection判断,代码会散得到处都是,维护起来很痛苦。我的做法是封装一个统一的模块,对外暴露简单的接口,内部处理兼容和降级。下面是一个精简版实现:
const NetworkStatus = (() => { const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; const listeners = new Set(); function getStatus() { if (!connection) { return { supported: false, effectiveType: '4g', downlink: 10, rtt: 100, saveData: false, type: 'unknown' }; } return { supported: true, effectiveType: connection.effectiveType || '4g', downlink: connection.downlink || 10, rtt: connection.rtt || 100, saveData: connection.saveData || false, type: connection.type || 'unknown' }; } function isWeakNetwork() { const status = getStatus(); return status.effectiveType === 'slow-2g' || status.effectiveType === '2g' || status.effectiveType === '3g'; } function shouldSaveData() { const status = getStatus(); return status.saveData || status.effectiveType === 'slow-2g' || status.effectiveType === '2g'; } function subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); } if (connection) { connection.addEventListener('change', () => { const status = getStatus(); listeners.forEach(fn => fn(status)); }); } return { getStatus, isWeakNetwork, shouldSaveData, subscribe }; })();这个模块的价值在于:业务代码只依赖isWeakNetwork()和shouldSaveData()这两个语义化方法,不需要关心底层是哪个浏览器、有没有前缀、支不支持。将来如果 API 有变化,只需要改这一个文件。
4.3 用实际请求数据做二次校准
浏览器给的effectiveType和rtt是估算值,不一定完全准确。我在项目里加了一层“运行时校准”:记录最近几次关键接口的实际耗时,如果实际耗时明显高于rtt推算的预期,就主动把网络状态往差的方向调整一档。
const requestSamples = []; function recordRequest(duration) { requestSamples.push(duration); if (requestSamples.length > 5) requestSamples.shift(); } function getCalibratedLevel() { if (requestSamples.length < 3) return NetworkStatus.getStatus().effectiveType; const avg = requestSamples.reduce((a, b) => a + b, 0) / requestSamples.length; if (avg > 3000) return '2g'; if (avg > 1000) return '3g'; return '4g'; }这层校准的意义在于:浏览器的估算有滞后性,而实际请求数据是“当下真实发生”的。两者结合,判断会更贴近用户的真实体感。这个思路我在多个项目里验证过,对于“网络刚变差但浏览器还没更新状态”的场景特别有效。
5. 几个容易踩的坑和我的处理方式
5.1 downlink 的单位和精度陷阱
downlink的单位是 Mbps,但它的精度只有 0.05 左右,而且浏览器会做取整处理。你可能会看到10、1.5、0.35这样的值,但不要指望它精确到小数点后好几位。更重要的是,这个值是预估峰值带宽,不是当前实时带宽。所以不要用它来做“当前能不能加载某个大文件”的判断,它更适合做“大概处于什么档位”的粗粒度分类。
我踩过的坑是:早期用downlink > 5来判断“高速网络”,结果发现很多 Wi-Fi 环境下downlink显示 10,但实际下载速度只有几百 KB/s。后来改成用effectiveType为主、downlink为辅,判断就稳多了。
5.2 change 事件不触发的情况
前面提过change事件触发频率不稳定,这里补充一个具体场景:当用户从 Wi-Fi 切到蜂窝数据时,type会变,change通常会触发;但如果只是 Wi-Fi 信号变弱、带宽下降,effectiveType可能从4g降到3g,这个变化不一定会立即触发change。我的处理方式是加一个定时轮询作为兜底:
let lastEffectiveType = null; setInterval(() => { const current = NetworkStatus.getStatus().effectiveType; if (current !== lastEffectiveType) { lastEffectiveType = current; // 手动触发一次状态变更处理 handleNetworkChange(current); } }, 5000);5 秒的轮询间隔是个折中值,既能及时感知变化,又不会太耗性能。这个兜底逻辑在 Chromium 上其实很少真正用到,但在一些行为不一致的浏览器上,它救过我好几次。
5.3 不要在页面加载最早期就依赖它
有一个很隐蔽的问题:在页面<head>里同步执行的脚本中读取navigator.connection,拿到的值可能是不准确的初始值。浏览器需要一点时间来采样和估算。我的经验是,至少等到DOMContentLoaded之后再读取,或者干脆在首次业务请求发起时再读。如果你在页面最顶部就用它来决定加载哪些资源,可能会做出错误判断。
提示:如果确实需要在早期做资源决策,建议先用一个保守的默认值(比如按 3g 处理),等
DOMContentLoaded后再根据真实状态做二次调整。
6. 我对这套 API 的实践体会
用了几年下来,我对NetworkInformation的定位是:它是一个很好的“辅助决策信号”,但不是一个可以单独依赖的“精确测量工具”。它的价值不在于告诉你网络有多快,而在于帮你把用户粗略地分成几档,然后针对不同档位做差异化的体验设计。
我现在的项目里,它主要承担三个角色:一是图片和视频的加载策略选择,二是省流量模式下的行为收敛,三是请求超时和重试策略的动态调整。这三件事都不需要极高的精度,但都能从“知道用户大概处于什么网络环境”中获益。
最后分享一个我一直在用的小技巧:把NetworkStatus模块的状态变化和前端埋点结合起来,记录用户在网络切换时的行为路径。这些数据能帮你发现很多平时注意不到的问题,比如“用户在 3g 环境下点击某个按钮后流失率特别高”,那这个按钮背后的逻辑就值得优化。网络状况不只是一个技术参数,它直接影响着用户能不能顺畅地完成他想做的事。把这件事做好,比堆再多花哨的功能都实在。