首页/文章/浏览器

优化浏览器渲染主线程的 api 都有哪些?怎么用?

2024-09-02
362213 分钟
...

为了提高浏览器的性能和用户体验,现代浏览器采用了一些技术来优化主线程的执行。主要有:

  • setTimeout 延迟加载 (使用不当可能适得其反)
  • web worker 多线程 (不能操作dom,主要计算密集型的任务)
  • requestIdleCallback 帧空闲时运行 (react18带🔥的,低优先级任务)
  • requestAnimationFrame 每帧都会运行 (主要做动画效果调优)

setTimeout 延迟加载非常常见,但是它有很多弊端,而且延迟时间并不准确。最佳的替代品应该是requestAnimationFrame,他的延迟时间要比setTimeout更精准,使用起来和setTimeout类同,只是不需要传延迟时间而已。

web worker在性能优化上经常用到,但是它不能操作dom,可以用来处理繁琐的计算逻辑。

requestIdleCallback它的目的是不要让浏览器再摸鱼了,赶紧起来干活。

浏览器的渲染进程的主线程,每秒重绘60帧,每一帧需要处理很多任务,除了html解析,还有开发者在JS中定义的其他任务,如微任务、元素事件任务、延时任务等。一般来说开发者可以自定义任务的执行时机,以前的时候都是利用setTimeout来通过重启task来改变任务执行时机,主线程一旦被阻塞,他的执行就会被无限延迟。体验非常狼狈。

浏览器放出2过分api:requestanimationframe和requestIdleCallback,他们的执行时机,完全由浏览器控制。

可以看出每个任务执行的时机确实是由浏览器自行决定的,这种方法适用于处理和渲染无关的低优先级事件。

调用时机:

requestanimationframe调用于下一帧重绘之前。

requestIdleCallback则略显复杂,分两种情况:

  • . 若当前帧有足够的空闲时间就调用,否则等下一帧
  • . 若当前帧没有足够的空闲时间,但是等待时间已经超时了,也会立即调用

react的fiber核心就是requestIdleCallback实现的。

<!DOCTYPE html>
<html>
	<body>
		<span>js耗时任务测试</span>
		<script>
			
			function processTaskTime(callback, ...args) {
				const startTime = new Date().valueOf();
				callback(...args);
				let endTime = new Date().valueOf();
				const processTime = endTime - startTime;
				console.log(`%c 执行 ${callback.name}(${args}) 消耗时间:` + processTime + '毫秒', 'color:green')
				return processTime;
			}

			
			function sumLoop(start, end) {
				let sum = 0
				for (let i = start; i < end; i++) {
					for (let j = start; j < end; j++) {
						tmp = sum;
						sum = tmp + i + j
					}
				}
				console.log(`从${start}${end}自相相加和为:` + sum)
				return sum
			}

			processTaskTime(sumLoop, 0, 15000)
		</script>
		<button onclick="processTaskTime(sumLoop,0,15000)"> 执行耗时任务 </button>
		<button onclick="javascript:console.log('click event')"> click </button>
	</body>
</html>

页面加载半天,才能出现结果。这就是典型的长任务,我们优化的目标。

第一种优化方案setTimeout

processTaskTime(sumLoop, 0, 15000);
 
setTimeout(()=>processTaskTime(sumLoop, 0, 15000),100)		    

执行速度感觉确实快了一点

第二种优化方案Web Worker




self.onmessage = function(event) {
  
  var data = event.data;
  
  
  var result = performCalculation(data);
  
  
  self.postMessage(result);
};

function performCalculation(data) {
  
  
  
  return data * 2;
}

主线程


var worker = new Worker('worker.js');


worker.onmessage = function(event) {
  
  var result = event.data;
  
  
  console.log('计算结果:', result);
};


var data = 5;
worker.postMessage(data);

主线程里面用 new Worker 创建 work 实例,然后在构造函数里面传入子线程执行文件的地址,这样子线程就可以和主线程通信了,利用onmessage和postmessage的方法相互传递数据。

页面加载速度得到了明显的提升。

第三种优化方案requestIdleCallback

requestIdleCallback是一个Web API,允许开发者在主线程空闲时去执行低优先级回调函数。这个函数的主要目的是使得开发者能够在不影响关键事件如动画和输入响应的情况下,执行后台或低优先级的任务。

不是每一帧都会执行,只有在浏览器主线程空闲的时候才会执行。

变成了秒出现页面。

第四种优化方案 requestAnimationFrame

<!DOCTYPE html>
<html>
	<body>
		<span>js耗时任务测试</span>
		<script>
			
			function processTaskTime(callback, ...args) {
				const startTime = new Date().valueOf();
				callback(...args);
				let endTime = new Date().valueOf();
				const processTime = endTime - startTime;
				console.log(`%c 执行 ${callback.name}(${args}) 消耗时间:` + processTime + '毫秒', 'color:green')
				return processTime;
			}

			
			function sumLoop(start, end) {
				let sum = 0
				for (let i = start; i < end; i++) {
					for (let j = start; j < end; j++) {
						tmp = sum;
						sum = tmp + i + j
					}
				}
				console.log(`从${start}${end}自相相加和为:` + sum)
				return sum
			}
      
      
      requestAnimationFrame(()=>{
        processTaskTime(sumLoop, 0, 15000)
      })
      
			
		</script>
		<button onclick="processTaskTime(sumLoop,0,15000)"> 执行耗时任务 </button>
		<button onclick="javascript:console.log('click event')"> click </button>
	</body>
</html>

执行代码以后,你会发现他的加载速度变成了4个里面最慢的哪一个,是不是很诧异?

setTimeout

requestIdleCallback requestAnimationFrame

他们三个都重新启动了宏任务

requestIdleCallback 注册的回调函数时,该回调函数会作为一个宏任务被添加到事件队列中。 requestIdleCallback 的回调函数在执行时是依赖于事件队列的管理的。当浏览器在空闲时段时调用。

主线程空闲的时候,说明事件队列其他的(与requestIdleCallback无关的)回调已经执行完了,requestIdleCallback就处于事件队列最前面。

使用场景

1️⃣埋点日志相关

  • 在用户有操作行为时(如点击按钮、滚动页面)进行数据分析并上报
  • 处理数据时往往会调用 JSON.stringify ,如果数据量较大,可能会有性能问题。
const queues = [];

document.querySelectorAll('button').forEach(btn => {
  btn.addEventListener('click', e => {
    
    pushQueue({
      type: 'click'
      
    }));
  
    schedule(); 
  });
});

function schedule() {
  requestIdleCallback(deadline => {
    while (deadline.timeRemaining() > 1) {
        const data = queues.pop();
        
    }
    
    if (queues.length !== 0) {
      
      schedule();
    }
  });
}

2️⃣预加载

例如当你需要处理一些数据,但这些数据不需要立即展示给用户时,可以在空闲时预处理这些数据

function prefetch(entry: Entry, opts?: ImportEntryOpts): void {
  if (!navigator.onLine || isSlowNetwork) {
    
    return;
  }

  requestIdleCallback(async () => {
    const { getExternalScripts, getExternalStyleSheets } = await importEntry(entry, opts);
    
    requestIdleCallback(getExternalStyleSheets);
    requestIdleCallback(getExternalScripts);
  });
}

3️⃣延迟执行

当你有一些非必须立刻执行的代码时,比如初始化某些非关键的UI组件,你可以使用 requestIdleCallback 来推迟这些任务的执行

不适合场景

1️⃣不适合操作dom&更新UI

执行时机不确定可能导致视觉难以预测 空闲回调执行的时候,当前帧已经结束绘制了,所有布局的更新和计算也已经完成。可能会引发回流重绘。 2️⃣不适合做一些耗时的长任务

虽然是在浏览器空闲执行,但依然运行在主线程上,耗时的长任务同样会导致帧率降低, 造成页面卡顿。 requestIdleCallback 不适合执行 DOM 操作,因为修改了 DOM 之后下一帧不一定会触发修改,主线程可能还被占据着。

react 并没有使用了 requestIdleCallback 来解决 stack 的问题,但 react 自主实现的调度算法与 requestIdleCallback 息息相关,那么为什么要放弃它而选择自主实现呢?

浏览器兼容性,目前并不是所有浏览器都支持这个 API 触发频率不稳定 FPS 远低于60, 这远远低于页面流畅度的要求(主要原因)

requestAnimationFrame 是一个浏览器提供的 JavaScript 方法,用于优化执行动画和其他循环操作的效率。它允许开发者在浏览器的下一次重绘之前调度一个回调函数,以确保动画在每一帧中都能够以最佳的性能和流畅度运行。

function animate() {
  

  
  requestAnimationFrame(animate);
}


requestAnimationFrame(animate);

使用场景

⭐ 当使用 setInterval 或 setTimeout 来执行循环操作或动画时,存在以下问题:

1️⃣不稳定的帧率:setInterval 和 setTimeout 方法是按照指定的时间间隔执行回调函数。然而,浏览器的重绘率(屏幕刷新率)通常是固定的,例如 60 Hz(每秒 60 帧)。如果指定的时间间隔小于重绘率,那么某些帧可能会被跳过,导致动画不连续或不流畅。反之,如果时间间隔大于重绘率,动画可能会显得卡顿。

2️⃣不可预测的性能:使用 setInterval 或 setTimeout 无法准确控制每一帧的执行时间。由于 JavaScript 是单线程的,如果在某一帧执行的回调函数需要较长时间来完成,那么下一帧的回调函数可能会被延迟执行,从而导致不稳定的性能表现。这可能会导致动画的延迟、卡顿或者整体性能下降。

3️⃣响应性差:由于 setInterval 或 setTimeout 是通过定时器触发回调函数,它们不考虑浏览器的渲染过程。这意味着即使浏览器当前正在进行重绘,回调函数也会被触发。这可能导致在关键渲染时刻执行 JavaScript 代码,从而影响页面的响应性能。

⭐ 相比之下,requestAnimationFrame 是专门为动画优化而设计的方法,解决了上述问题:

1️⃣平滑的帧率:requestAnimationFrame 的回调函数会在每一帧开始绘制之前被调用,与浏览器的重绘率同步。这意味着动画将以流畅的 60 帧/秒(或其他重绘率)运行,产生连续而平滑的动画效果。

2️⃣更好的性能控制:requestAnimationFrame 的回调函数会在浏览器准备好绘制下一帧时被调用,确保每一帧的执行时间在可接受范围内。这有助于提供更稳定的性能,避免过长的回调导致的性能问题。

3️⃣更好的响应性能:requestAnimationFrame 会自动与浏览器的渲染过程同步。如果页面不可见或最小化,requestAnimationFrame 将自动暂停,避免不必要的计算和功耗。这对于提高页面的响应性能和用户体验非常重要。

<!DOCTYPE html>
<html>
	<body>
		<div id="box" style="height: 20px; width: 20px; background-color: blue;position: relative;"></div>
		<script>
			const box = document.querySelector('#box');
			let position = 0; 
			const speed = 20; 
			
			function animate() {
				
				position += speed;
				
				box.style.left = position + 'px';
				if (position < 500) {
					window.requestAnimationFrame(animate)
				}
			}
			
			window.requestAnimationFrame(animate)
		</script>
	</body>
</html>

		const [status,setStatus]=useState(0);
		function splitTaskRender(){
			setStatus(++status)
			if(status<3){
				window.requestAnimationFrame(splitTaskRender)
			}
		}
		window.requestAnimationFrame(splitTaskRender)
		
		{status>1&&<div>Module 1</div>}
		{status>2&&<div>Module 2</div>}
		{status>3&&<div>Module 3</div>}

性能问题 requestAnimationFrame 回调函数中执行的任务太耗时,会导致以下问题:

1️⃣卡顿和掉帧:如果回调函数的执行时间超过了每帧的时间限制(通常为 16.7 毫秒),就会导致浏览器无法及时完成绘制,导致页面出现卡顿和掉帧的现象。

2️⃣资源占用过多:执行耗时任务可能会占用过多的 CPU 资源,导致浏览器的性能下降。这可能会影响其他页面元素的响应性能,导致页面变得不流畅,用户可能无法顺利地与页面进行交互。

优化策略 1️⃣分割任务:将耗时任务分割为多个较小的子任务,并在多个帧中逐步执行。这样可以避免单个帧中执行时间过长,减少卡顿和掉帧的问题。(参考上文 首屏多模块加载优化)

2️⃣优化算法:通过优化算法或使用更高效的数据结构,尽量减少任务的执行时间。

3️⃣降低帧率:如果任务确实无法在单个帧中完成,可以通过降低 requestAnimationFrame 的调用频率,减少任务的执行次数,以达到平衡可接受的性能和任务完成的目标。

let frameCount = 0;
const frameInterval = 10; 

function animate() {
  frameCount++;

  if (frameCount % frameInterval === 0) {
    
    

    
    frameCount = 0;
  }

  requestAnimationFrame(animate);
}

requestAnimationFrame(animate);

4️⃣使用 Web Worker:对于非UI相关的耗时任务,可以考虑使用 Web Worker 在后台线程中执行,以充分利用多线程并减少对主线程的影响。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
浏览器2025-07-30
浏览器内核到底有哪些?它们各自又有什么区别?
这问题其实很多刚入行的同学经常搞混,所以今天我来一次性讲明白—— 浏览器内核 到底有哪些?它们各自又有什么区别? 说白了,浏览器内核就是浏览器的大脑,分两部分: 渲染引擎(Rendering Engine) :负责网页的排版渲染,HTML、CSS统统交给它。 JS引擎(JavaScript Engine) :处理JavaScript脚本逻辑,比如你点按钮后弹出对话框这种行为。 1)常见浏览器内核盘点 下面我直接列几个主流的浏览器内核,顺...
学习面试
浏览器2025-03-07
浏览器缓存及优化手段
前言 当我们通过url首次访问某个网站的时候,我们如果使用浏览器的工具查看网络请求,会发现有很多http请求,但是当我们下一次再访问时,这些请求都不会发送而是展现在了页面上,而这就是浏览器缓存发挥的作用。 浏览器缓存 浏览器缓存是指将页面长时间不更新的资源缓存到浏览器上,下次访问页面时该部分长时间不更新的资源直接从缓存中获取,从而减少了网络请求次数,提高了页面的加载速度。浏览器的缓存方式主要分为两种: 强缓存 和 协商缓存 ,下面将为大...
学习面试
浏览器2024-09-10
深入理解浏览器的事件循环机制
在现代Web开发中,JavaScript作为浏览器中最常用的编程语言,其执行机制深刻影响着Web应用的性能和用户体验。事件循环(Event Loop)作为JavaScript处理异步操作的核心机制,是理解JavaScript执行模型的关键。本篇文章从多个维度去分析事件循环的机制,希望可以帮大家更加深入的了解它。 在讨论事件循环之前,理解浏览器的执行环境是至关重要的。浏览器是一个多线程环境,包含以下主要线程: GUI 渲染线程(GUI R...
学习面试
浏览器2024-07-25
图解浏览器的多进程渲染机制
观察浏览器的任务管理器可以发现,打开浏览器的一个页面需要多个进程,包括浏览器进程、GPU进程、网络进程、渲染进程等,有插件的话还会包括各种插件进程(Chrome选项 更多工具 任务管理器)。 本文将聚焦于 浏览器的各个进程间是如何配合,将页面呈现给用户的。 📌 你将了解到 1. 浏览器在历史发展过程中,其进程架构做了哪些调整,为什么这样调整,以及解决了哪些问题? 2. 从用户在地址栏输入URL,到页面渲染完成这之间发生了什么?回流和重...
学习面试
浏览器2024-06-11
localStorage是同步还是异步的?为什么
点赞 \+ 收藏 === 学会🤣🤣🤣 首先明确一点,localStorage是同步的 是 Web Storage API 的一部分,它提供了一种存储键值对的机制。 的数据是持久存储在用户的硬盘上的,而不是内存。这意味着即使用户关闭浏览器或电脑, 中的数据也不会丢失,除非主动清除浏览器缓存或者使用代码删除。 当你通过 JavaScript 访问 时,浏览器会从硬盘中读取数据或向硬盘写入数据。然而,在读写操作期间,数据可能会被暂时存放...
学习面试
浏览器2024-04-15
构建浏览器工作原理知识体系(开篇)
本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究! 作为一个前端工程师,浏览器是我们密不可分的朋友,想要深耕前端,就必须和浏览器“交心”💞。 为什么你觉得偶尔看浏览器的工作原理,但总是忘呢😵‍💫,因为你没有形成一个完整的知识网络,你的记忆是碎片化的。正如人的神经网络,只有当你的记忆相互依赖,相互链接,才能形成长期稳定的记忆。 所以本文我将用一条知识线将浏览器工作原理的知识串联起来,因为本文的...
学习面试

评论

请登录后发表评论

去登录
加载评论中...

目录