这篇文章里,我整理了 8 个很强、却依然被大量低估的浏览器能力。它们不算花哨,但真的很实用。有些功能,甚至会直接改变你对“前端到底该怎么做”的理解。
所以,别急着装依赖。
先往下看。也许你会发现,自己这些年其实绕了不少远路。
1. 输入框聚焦了,父元素也该有反应
给输入框本身加聚焦样式,这谁都会。
难的是,当你想让父容器也跟着变样时,事情就开始变得不对劲了。
你会下意识想:
“是不是得加事件监听?” “focus 的时候加个 class,blur 的时候再删掉?” “嗯……好像也不是很复杂?”
结果写着写着,几十行 JavaScript 就冒出来了。 你只是想高亮一个容器,却突然开始怀疑自己到底在干嘛。
其实,浏览器早就给了答案:
:focus-within
它干的事情非常直白:
只要一个容器里的任意子元素获得焦点,这个容器本身就可以被选中并应用样式。
.form-field { border: 1px solid #ccc; padding: 12px; } .form-field:focus-within { border-color: hotpink; }<div class="form-field"> <input placeholder="Type something meaningful..." /> </div>这类场景最常见的地方,就是表单 UI。你原本只是想让整块输入区域在用户操作时更明显一些,结果以前得靠 JS 才能完成;现在,用一条 CSS 规则就够了。
而且支持情况也很稳,主流浏览器基本都没什么问题。
2. 用户断网了,不一定要整一套大工程
只要你做过 PWA,或者稍微碰过需要离线体验的 Web 应用,就一定想过这个问题:
用户突然没网了,页面该怎么办?
可能他在地铁里。 可能他刚进电梯。 也可能只是家里的 Wi-Fi 又发疯了。
很多人一碰到这个问题,就容易往复杂方向想: 先搭一个离线缓存系统,再做一个请求重试层,再补一个本地队列,还得考虑失败回放和数据同步……
这些当然都可能有价值。 但在你把整套大工程搬出来之前,其实应该先问一句:
浏览器最基础的能力,你用了吗?
浏览器原生就提供了两个事件:
online和offline
你可以在网络状态变化时立刻感知到,然后做出对应处理——比如提示用户、暂停请求、把操作临时存本地,或者等网络恢复后再同步。
window.addEventListener("offline", () => { alert("You are offline. Your internet is gone"); }); window.addEventListener("online", () => { alert("You're back. welcome back"); });这东西特别适合做“先别慌”的第一层体验处理。
当然,确实有个现实问题要提醒: 浏览器判断“在线”,并不等于你的后端服务一定可用。它只能说明:设备看起来是联网的。
但即便如此,这两个事件依然非常值得用。因为在很多场景下,用户需要的并不是一套巨型架构,而是一个及时、明确、足够友好的反馈。
3. 别现在跑,等浏览器有空再跑
第一次看到这个 API 的时候,我也觉得它有点鸡肋。
它的意思差不多就是:
“浏览器空闲的时候,再帮我执行这段代码。”
我当时的第一反应是:
“这有啥意义?代码不就是该执行就执行吗?”
后来慢慢才意识到,不是所有任务都必须马上做。
有些事情根本不急。 比如埋点统计、预加载、不影响首屏的数据处理,或者某些完全可以放在后台慢慢跑的逻辑。
说白了,页面正在渲染、组件正在更新、用户正在交互的时候,你真的不该让这些低优先级任务来抢主线程。
这就是requestIdleCallback最适合发挥作用的地方。
它会把时机选择交给浏览器: 什么时候空一点,什么时候不影响体验,再执行这段代码。
function showMessage() { console.log("👋 This ran when the browser was idle!"); } if ("requestIdleCallback" in window) { requestIdleCallback(showMessage); } else { setTimeout(showMessage, 0); }这类能力特别适合处理:
分析和埋点
后台数据整理
可以延迟的非关键任务
不该阻塞主线程的琐碎工作
一旦你开始用“高优先级 / 低优先级”的方式看待前端任务,很多原本混在一起的逻辑,都会变得清楚。
顺便提醒一句:Safari 目前还不完整支持,所以最好保留一个 fallback。
4. 动画的节奏,应该让浏览器来掌控
很多人第一次写动画时,都会先想到setInterval。
比如,让一个盒子往右移动:
setInterval(() => { box.style.left = box.offsetLeft + 5 + "px"; }, 16);它确实能动。 而且刚开始看,好像也没什么问题。
可只要时间一长,你就会慢慢发现:
不够顺。 有时会卡。 节奏不稳。 在某些设备上尤其明显。
问题在于,setInterval根本不关心浏览器什么时候真正重绘。它只是粗暴地按你设定的时间间隔去“猜”——大概每 16ms 跑一次。
但浏览器的刷新节奏不是这么工作的。
更合适的方式,是requestAnimationFrame。
function moveBox() { box.style.left = box.offsetLeft + 5 + "px"; requestAnimationFrame(moveBox); } requestAnimationFrame(moveBox);这两种写法的差别,表面看不大,实质上却很关键。
前者是在说:
“你每隔 16ms 跑一次吧,差不多就行。”
后者是在说:
“你准备重绘的时候,再帮我执行。”
而动画这种事,真正需要的恰恰就是“跟着浏览器节奏走”。一旦把时机交回给浏览器,流畅度通常就会明显改善。
5. 让组件根据自己,而不是根据屏幕,决定怎么变
这些年一提响应式,大家第一反应基本都是媒体查询。
它当然好用,而且过去也确实解决了大量问题。
可问题是,媒体查询看的始终是整个视口。 而现实中的组件,很多时候根本不是铺满全屏的。
比如一个卡片放在大屏里的窄侧边栏里; 又或者它被塞进一个宽页面中的小区域里。
这时候,屏幕明明很宽,可组件自己其实很挤。 你却还在按照 viewport 的尺寸做判断。
于是布局开始出问题。 然后你就开始补 hack。 再然后,你开始怀疑是不是自己响应式没写对。
其实你真正想表达的,往往不是:
“如果屏幕足够宽……”
而是:
“如果这个组件所在的容器足够宽……”
这就是Container Queries出现的意义。
它让组件可以根据自身容器的尺寸来响应,而不是永远盯着整个屏幕。
.card-wrapper { container-type: inline-size; } .card { display: grid; gap: 10px; } @container (min-width: 400px) { .card { grid-template-columns: 1fr 2fr; } }这样一来,同一张卡片放在不同地方,就可以自然长出不同布局:
在窄侧边栏里,它自动堆叠
在宽内容区里,它自动并排
不用额外判断
不用写一堆例外情况
也不用再问“为什么 iPad 上偏偏这里炸了”
这类能力,真正提升的不是炫技程度,而是组件独立性。 你终于可以把组件写得更像组件,而不是“绑定在页面结构上的半成品”。
6. 别再拿 Math.random() 当唯一 ID 生成器了
很多人都写过类似这种代码:
const uniqueId = Math.random().toString(36).slice(2);看起来没毛病。
够短。 够方便。 够像随机。 而且跑起来也确实能用。
于是它就这么被你心安理得地放进项目里了。
可问题在于,Math.random()从来就不是为“生成可靠唯一 ID”设计的。
它的问题并不总会立刻暴露,但隐患一直都在:
它不是真正强随机
实现依赖浏览器引擎
可能出现模式性
时间久了,碰撞概率会越来越难忽视
换句话说,它属于那种:
“刚开始看起来够用了,等真出事时你才知道不够。”
浏览器其实早就给了更靠谱的方案:crypto.getRandomValues()
const bytes = new Uint8Array(8); crypto.getRandomValues(bytes); const uniqueId = Array.from(bytes) .map(b => b.toString(16).padStart(2, "0")) .join("");这类方式更合理的原因在于:
随机性更强
熵更高
模式更少
冲突概率更低
本质上,你是在把“随机”这件事交给浏览器更专业的能力来处理,而不是继续拿一个本就不为此而生的函数硬顶。
尤其是当 ID 不只是前端临时玩玩,而是真正参与状态、节点映射、缓存或数据记录时,这种区别很容易变成线上 bug 和稳定系统之间的分界线。
7. 别一做弹窗就先装 Modal 库
几乎每个项目都会走到这一步:
“我们要做个弹窗。”
然后事情就会迅速开始失控。
装库。 调 z-index。 补焦点管理。 修滚动穿透。 再顺手处理 ESC、点击遮罩关闭、无障碍、层级冲突……
最后你会发现,自己为了一个弹窗,忙得像在造操作系统。
但现在,浏览器其实已经很明确地告诉你:
这件事,我能自己来。
它给出的答案就是:<dialog>
一个原生的对话框元素。
<button id="openBtn">Delete Account</button> <dialog id="modal"> <h3>⚠️ Delete Account</h3> <p>This action cannot be undone. Are you sure?</p> <div class="actions"> <button id="cancelBtn">Cancel</button> <button id="confirmBtn">Yes, Delete</button> </div> </dialog>为什么它值得认真看待?
因为很多你过去需要自己补的事,它已经原生处理了:
自带对话框行为
焦点管理更自然
可访问性更友好
背景区域会失活
ESC 可关闭
不必靠一堆 CSS 和 JS 小心翼翼拼出来
当然,复杂弹层系统并不是从此都不需要第三方方案了。 但至少对于“我们只是需要一个正常弹窗”这种场景,很多项目真的没必要一上来就把依赖装满。
有时候你以为自己缺的是一个库, 其实你缺的只是多看一眼平台本身。
8. 浏览器其实已经会“听你说话”了
很多人一想到语音输入,脑子里立刻冒出来的都是重型方案:
AI 模型。 语音服务。 第三方 SDK。 一堆复杂配置。 甚至默认觉得“这东西肯定很重”。
但现实有时候比想象简单得多。
浏览器本身,其实已经提供了语音识别能力。 也就是Speech Recognition API。
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; const btn = document.getElementById("startBtn"); const box = document.getElementById("box"); if (SpeechRecognition) { const recognition = new SpeechRecognition(); btn.onclick = () => { recognition.start(); }; recognition.onresult = (e) => { const text = e.results[0][0].transcript.toLowerCase(); box.textContent = text; box.style.background = text; }; } else { btn.textContent = "Not supported 😢"; }这里发生的事情其实很直接:
你点击开始
浏览器开始监听
用户开口说话
浏览器把语音转成文字
结果立刻交给你使用
没有自己训练模型。 没有复杂接入流程。 也不一定非得拉一堆大库进来。
这并不意味着它适合所有语音场景。 但至少说明了一件事:有些你以为必须靠“更大方案”解决的问题,浏览器早就给出了足够好用的第一层能力。
最后:
精通 React 面试:从零到中高级(针对面试回答)
CSS终极指南
Vue 设计模式实战指南
20个前端开发者必备的响应式布局
深入React:从基础到最佳实践完整攻略
python 技巧精讲
React Hook 深入浅出
CSS技巧与案例详解
vue2与vue3技巧合集
全栈AI·探索:涵盖动效、React Hooks、Vue 技巧、LLM 应用、Python 脚本等专栏,案例驱动实战学习,点击二维码了解更多详情。