JS线程阻塞监控
目标
由于JS单线程的语言特性,浏览器js中大部分事件都是以异步回调的形式被处理。例如浏览器中对用户的输入进行响应,实际上是这样一个流程:
- 浏览器进程监听到用户的输入(键盘或鼠标)
- 创建相应的event对象
- 将event的回调事件推入下一事件循环
- 等待js线程完成当前事件循环
- 根据js事件模型,按照捕获阶段、目标阶段、冒泡阶段的顺序,依次执行相应的回调函数
- 假如事件的
preventDefault
没有被调用,那么浏览器才会执行默认的处理,如:输入框字符上屏
假如在4和5中有long task长时间占据js执行线程,那么1和6之间的间隔就会变大,用户会明显感知到页面的反馈不及时,也就是卡顿。
因此,假如我们需要对卡顿的频率和严重情况进行监控时,或是希望监控long task情况保证研发质量时,就需要对js线程阻塞的时间进行监控。
获取js线程阻塞时间
那如何计算js线程的阻塞时间呢?我们可以转而计算:浏览器监听到用户输入的时间,和我们实际回调函数的执行时间,两者的差值就可以近似的看作是js线程被阻塞的时长。
事件创建时间
翻阅MDN的文档可以找到,Event上存在timeStamp属性,其值就是事件创建的时间戳。然而在chrome
上实际尝试后可以看到,这个值远远小于Date.now()
返回的时间戳,比如是16662.19999999553
,这是为什么呢?
实际上,根据W3C规范中的事件模型对于该属性的描述:
Used to specify the time (in milliseconds relative to the epoch) at which the event was created. Due to the fact that some systems may not provide this information the value of
timeStamp
of typeDOMTimeStamp
, readonlytimeStamp
may be not available for all events. When not available, a value of 0 will be returned. Examples of epoch time are the time of the system start or 0:0:0 UTC 1st January 1970.
也就是说event.timeStamp
是一个相对时间戳,单位是毫秒,代表的是事件的创建时间。假如无法获取创建时间,则读取到的是0
。这里的相对并没有指定是UNIX毫秒时间戳,所以这个属性的实际上是一个高精时间戳(High Resolution Time)
高精时间戳
高精时间戳(High Resolution Time)是一个高精度的、最高可以精确到微秒级别的、不随系统时间偏移的时间戳。
Date.now()
的精确程度只有毫秒级别,更重要的是,它是基于系统(本地)时间的,假如系统时间有偏差时(例如系统时间快/慢了数分钟)返回的时间戳也是有偏差的。而且由于读取的是本地时间,它并不能保证是单调递增的。假如某一时刻系统时间被修改,这一时刻之后的返回值是可能小于之前的返回值的。而高精时间戳可以保证单调递增,而且还是以恒定速率单调递增的。
高精时间戳也是一个相对时间戳,它的0时刻并不是固定的,而是上下文创建的时间。因此,高精时间戳也可以看作是相对performance.timeOrigin后经过的时间,而performance.timeOrigin
代表的是上下文创建时的、相对于UNIX 0时刻的高精度时间戳。
当前时刻高精时间戳
performance
上的now()方法返回的是当前时刻的高精时间戳,也就是相对performace.timeOrigin
之后经过的时间。
计算js线程阻塞时间
因为event.timeStamp
和performance.now
都是基于performance.timeOrigin
的高精时间戳,因此它们可以直接相减,结果就是当前时刻与事件创建时刻之间的差值,也就是js线程阻塞的时间了。
Demo
这里以<input>
框与keydown
事件举例
// 获取关键元素的dom节点 const dom = document.querySelector('input'); // 模拟long task阻塞 dom.addEventListener('keydown', event => { if(event.key === 'q') { console.time('long task'); const arr = Array.from({ length: 1000 * 1000 }) .map((_, i) => i) .sort(_ => Math.random() - 0.5); console.timeEnd('long task'); } }); // 获取每次输入的js阻塞时间 dom.addEventListener('keydown', event => { const costTime = performance.now() - event.timeStamp; console.log('按键', event.key, 'js线程阻塞时间:', costTime); });
兼容性
兼容性方面不用顾虑,十年以内的浏览器版本基本都能使用,哪怕IE也能轻松支持。