JavaScript是一门动态、弱类型、解释型的脚本语言,与C、C++等先编译后执行的语言不同,JS实在程序运行过程中进行逐行解释,它的灵活性同时也给带来一些问题,比如代码的可读性、可维护性、可测试性以及性能的降低。而通过减少重绘和重排、使用节流和防抖技术、使用性能分析工具等方式可以优化JavaScript代码来提高性能。这里进行一些介绍和实践示例展示。
减少重绘和重排
首先,先理解页面生成的过程:
1.HTML 被 HTML 解析器解析成 DOM 树;
2.CSS 被 CSS 解析器解析成 CSSOM 树;
3.结合 DOM 树和 CSSOM 树,生成一棵渲染树(Render Tree),这一过程称为 Attachment;
4.生成布局(flow),浏览器在屏幕上“画”出渲染树中的所有节点;
5.将布局绘制(paint)在屏幕上,显示出整个页面。
第4,5步是最耗时的部分,这两步合起来,就是我们通常所说的渲染。
渲染:在页面的生命周期中,网页生成的时候,至少会渲染一次。在用户访问的过程中,还会不断触发重排(reflow)和重绘(repaint)
不管页面发生了重绘还是重排,都会影响性能,最可怕的是重排,会使我们付出高额的性能代价,所以我们应尽量避免。
重排(reflow)
当DOM的变化影响了元素的几何信息(元素的的位置和尺寸大小),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排。
重排也叫回流,简单的说就是重新生成布局,重新排列元素。
会发生重排的情况:
-
页面初始渲染,这是开销最大的一次重排 添加/删除可见的DOM元素 改变元素位置
-
改变元素尺寸,比如边距、填充、边框、宽度和高度等
-
改变元素内容,比如文字数量,图片大小等 改变元素字体大小
-
改变浏览器窗口尺寸,比如resize事件发生时
-
激活CSS伪类(例如::hover)
-
设置 style 属性的值,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次
-
查询某些属性或调用某些计算方法:offsetWidth、offsetHeight等,除此之外,当我们调用 getComputedStyle方法,或者IE里的 currentStyle 时,也会触发重排,原理是一样的,都为求一个“即时性”和“准确性”。
会引起重排的常见属性: width、height、margin、padding、display、border-width、border、position、overflow、font-size、vertical-align、min-height、clientWidth、clientHeight、clientTop、clientLeft、offsetWidth、offsetHeight、offsetTop、offsetLeft、scrollWidth、scrollHeight、scrollTop、scrollLeft
会引起重排的常见方法: scrollIntoView()、scrollTo()、getComputedStyle()、getBoundingClientRect()、scrollIntoViewIfNeeded()
重排影响的范围:
由于浏览器渲染界面是基于流式布局模型的,所以触发重排时会对周围DOM重新排列,影响的范围有两种:
-
全局范围:从根节点html开始对整个渲染树进行重新布局。
-
局部范围:对渲染树的某部分或某一个渲染对象进行重新布局。
重绘(repaints)
当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程,叫做重绘。
会引起重绘的常见属性: color、border-style、visibility、background、text-decoration、background-image、background-position、background-repeat、outline-color、outline、outline-style、border-radius、outline-width、box-shadow、background-size、
优化
减少重排范围
我们应该尽量以局部布局的形式组织html结构,尽可能小的影响重排的范围。
-
尽可能在低层级的DOM节点上,而不是像上述全局范围的示例代码一样,如果你要改变p的样式,class就不要加在div上,通过父元素去影响子元素不好。
-
不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局。那么在不得已使用table的场合,可以设置table-layout:auto;或者是table-layout:fixed这样可以让table一行一行的渲染,这种做法也是为了限制reflow的影响范围。
减少重排次数
-
样式集中变更,批量更新DOM
频繁改变元素样式会引起重排。尽量使用 CSS 类一次性应用样式变更。对于一个静态页面来说,明智且可维护的做法是更改类名而不是修改样式,对于动态改变的样式来说,相较每次微小修改都直接触及元素,更好的办法是统一在 cssText 变量中编辑。也可以将这些修改封装在一个函数中,一次性进行更新。
示例:
// × var left = 10; var top = 10; el.style.left = left + "px"; el.style.top = top + "px"; // √ el.style.cssText += "; left: " + left + "px; top: " + top + "px;"; // top和left的值是动态计算而成 // √ el.className += " className"; // 添加样式类 element.classList.add('my-style'); // 移除样式类 element.classList.remove('my-style'); function updateStyles() { element1.style.color = 'red'; element2.style.backgroundColor = 'blue'; // ... elementN.style.display = 'none'; } -
分离读写操作,避免强制同步布局
DOM 的多个读操作(或多个写操作),应该放在一起。不要两个读操作之间,加入一个写操作。在获取某些属性(如offsetWidth、offsetHeight等)时,会触发强制同步布局,导致重绘和重排。尽量避免频繁获取这些属性,或者使用缓存来减少获取次数。
示例:
// × 强制刷新 触发四次重排+重绘 div.style.left = div.offsetLeft + 1 + 'px'; div.style.top = div.offsetTop + 1 + 'px'; div.style.right = div.offsetRight + 1 + 'px'; div.style.bottom = div.offsetBottom + 1 + 'px'; // √ 缓存布局信息 相当于读写分离 触发一次重排+重绘 var curLeft = div.offsetLeft; var curTop = div.offsetTop; var curRight = div.offsetRight; var curBottom = div.offsetBottom; div.style.left = curLeft + 1 + 'px'; div.style.top = curTop + 1 + 'px'; div.style.right = curRight + 1 + 'px'; div.style.bottom = curBottom + 1 + 'px'; -
使用requestAnimationFrame
让浏览器在下一次重绘之前执行指定的函数。这样可以将多个DOM操作合并到一次重绘中,提高性能。
示例:
function updateDOM() { requestAnimationFrame(() => { element.style.width = '100px'; element.style.height = '100px'; element.style.opacity = '0.5'; // ... }); }
使用节流和防抖技术
节流(throttle)
指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。
<script>
// 1.封装节流函数
function throttle(fn, time) {
//3. 通过闭包保存一个 "节流阀" 默认为false
let temp = false;
return function () {
//8.触发事件被调用 判断"节流阀" 是否为true 如果为true就直接trurn出去不做任何操作
if (temp) {
return;
} else {
//4. 如果节流阀为false 立即将节流阀设置为true
temp = true; //节流阀设置为true
//5. 开启定时器
setTimeout(() => {
//6. 将外部传入的函数的执行放在setTimeout中
fn.apply(this, arguments);
//7. 最后在setTimeout执行完毕后再把标记'节流阀'为false(关键) 表示可以执行下一次循环了。当定时器没有执行的时候标记永远是true,在开头被return掉
temp = false;
}, time);
}
};
}
function sayHi(e) {
// 打印当前 document 的宽高
console.log(e.target.innerWidth, e.target.innerHeight);
}
// 2.绑定事件,绑定时就调用节流函数
// 绑定是就要调用一下封装的节流函数 触发事件是触发封装函数内部的函数
window.addEventListener('resize', throttle(sayHi, 2000));
</script>
防抖(debounce)
指触发事件后 n 秒后才执行函数,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
简单版本示例:
function debounce(func, wait) {
let timeout;
return function () {
let context = this; // 保存this指向
let args = arguments; // 拿到event对象
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
防抖如果需要立即执行,可加入第三个参数用于判断,实现如下:
function debounce(func, wait, immediate) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout); // timeout 不为null
if (immediate) {
let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会再次触发
timeout = setTimeout(function () {
timeout = null;
}, wait)
if (callNow) {
func.apply(context, args)
}
}
else {
timeout = setTimeout(function () {
func.apply(context, args)
}, wait);
}
}
}
应用场景
防抖(debounce)
1.search搜索时,用户在不断输入值时,用防抖来节约请求资源。
2.手机号、邮箱等验证输入检测
3.窗口大小resize。调整完窗口后,计算窗口大小,防止重复渲染。
节流(throttle)
1.鼠标不断点击触发,mousedown(单位时间内只触发一次)
2.监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断
区别
相同点:
都可以通过使用 setTimeout 实现 目的都是,降低回调执行频率。节省计算资源
不同点:
-
函数防抖,在一段连续操作结束后,处理回调,利用clearTimeout 和 setTimeout实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能
-
函数防抖关注一定时间连续触发的事件,只在最后执行一次,而函数节流一段时间内只执行一次
例如,都设置时间频率为 500ms,在 2 秒时间内,频繁触发函数,节流,每隔 500ms 就执行一次。防抖,则不管调动多少次方法,在 2s 后,只会执行一次
使用性能分析工具
-
浏览器的开发者工具: 浏览器提供了内置的开发者工具,可以查看网页的性能数据、内存使用情况、网络请求等。
-
Lighthouse: 是一款开源工具,可以分析网页的性能、可访问性、最佳实践等方面,并给出改进建议。
-
Chrome DevTools Performance 面板: 可以记录和分析页面的性能数据,包括 CPU 使用、内存占用、事件处理等。
-
Webpack Bundle Analyzer: 如果你使用 Webpack 进行打包,这个工具可以帮助你可视化分析打包后的 bundle 大小和模块依赖关系。
-
Chrome Trace: 生成详细的时间线跟踪,用于分析 JavaScript 执行、布局、绘制等性能问题。
懒加载和预加载
-
懒加载(Lazy Loading): 将页面资源(如图片、视频)的加载延迟到它们进入用户视野之前,从而加快初始页面加载速度。
例如路由的预加载(基于react和react-router使用的懒加载):
const NewsRouter: React.FC = () => { const element = useRoutes([ { path: "/home", element: LazyLoad("home/Home"), }, { path: "/", element: <Navigate to="/home" />, }, { path: "/user-manage/list", element: LazyLoad("user-manage/UserList"), }, ... ]); return <div>{element}</div> }; //路由懒加载的封装(按需加载) export const LazyLoad = (path: string) => { const Comp = React.lazy(() => import(`./${path}`)); //导入下载依赖的模块 return ( <React.Suspense fallback={<p>加载中...</p>}> <Comp /> </React.Suspense> ); }; -
预加载(Preloading): 在页面加载后,提前加载未来可能需要的资源,例如,当用户点击链接时,相关页面的资源已经在后台加载好。
代码优化和缓存
-
代码优化: 通过删除不必要的代码、减少循环嵌套、避免过多的全局变量等方式,优化 JavaScript 代码的执行效率。
-
使用缓存: 合理使用浏览器缓存、CDN 缓存等,减少不必要的网络请求。
综上,在大项目中,通过优化js代码来提高性能是很必要的任务,通过本文的讨论,我们可以应对大部分性能问题,以提升页面性能和用户体验。