web脚本造成浏览器减慢
打开一个新闻网站,脚减慢滚动两下,本造风扇开始转。成浏再切到另一个标签页,览器鼠标移动都带残影。脚减慢你以为是本造电脑老了,其实八成是成浏页面上某个脚本在偷偷跑循环。
上周我把一个客户的览器项目丢进Chrome的Performance面板录了十秒。结果很直白:主线程有6.2秒被一个叫trackScrollDepth的脚减慢函数占着。这个函数每触发一次scroll事件就遍历一遍所有DOM节点,本造算每个元素距离视口顶部的成浏距离。页面上一共3400个节点。览器滚动一次,脚减慢遍历3400次。本造用户滚一下滚轮,成浏这个函数跑三遍。
这不是个例。我见过一个电商详情页,光是第三方脚本就加载了17个。其中一个比价插件每500毫秒发一次requestAnimationFrame,在回调里读document.body.scrollHeight。这个属性会强制浏览器重新计算布局。500毫秒一次,页面永远处在“即将重排”的状态。用户滑动时,帧率从60掉到22。肉眼可见的卡。
脚本拖慢浏览器的路径其实就三条。第一条是长任务阻塞主线程。一个函数跑超过50毫秒,浏览器就没法响应用户输入。我测过一个“智能推荐”脚本,它在DOMContentLoaded之后同步执行了1.8秒。这1.8秒里,页面上的按钮点下去没反应,输入框敲不进字。用户以为死机了。
第二条是频繁的强制同步布局。脚本先改样式,紧接着读offsetHeight,再改样式,再读offsetWidth。浏览器本来可以批量处理这些读写,但脚本的读写交替逼着它每次都要先算完布局再返回结果。一秒钟搞个几百次,CPU直接拉满。有个做动画的库叫Velocity.js,老版本里有个tween函数就犯这毛病。后来作者在文档里专门写了一节叫“避免布局抖动”。
第三条最隐蔽:定时器泄漏。一个轮播图组件用setInterval每3秒切一张图。组件被移除后,定时器没清。这个定时器还在跑,还在操作已经被垃圾回收的DOM引用。浏览器控制台不报错,但内存曲线一直往上走。开十个这样的标签页,风扇不转才怪。
怎么判断是不是脚本的锅?不用装插件。Chrome DevTools里按Ctrl+Shift+P,输入“frame rendering stats”,勾上。页面上会多一个小浮层,显示实时FPS。滚动页面,FPS低于50就说明有问题。再打开Performance面板,点录制,滚两下,停止。看Main轨道里有没有长条的黄色块。有,那就是脚本在占着主线程。
修起来也不复杂。滚动监听加passive: true,告诉浏览器这个监听器不会阻止滚动,浏览器就能异步处理。DOM读写分离,用requestAnimationFrame把读操作集中到一帧的开头,写操作放到末尾。定时器在组件卸载时用clearInterval清掉。第三方脚本用async或defer加载,别让它们挡着首屏渲染。
我自己的博客之前也卡。后来发现是代码高亮库highlight.js在每篇文章加载时同步解析所有pre标签。一篇文章二十个代码块,每个块平均三百行。解析一次要四百多毫秒。改成defer加载,再把高亮操作扔进requestIdleCallback,滚动立刻顺了。FPS从31回到58。
浏览器本身不慢。慢的是那些没想清楚就塞进去的脚本。它们像一群在狭窄走廊里来回搬箱子的人,每个人都觉得自己只搬了一小会儿,但走廊堵死了。