你需要知道的网页渲染性能优化方法(上),面试技巧的书籍

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Web前端全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024c (备注前端)
img

正文

  1. 浅析 requestAnimationFrame

  2. 告别定时器,走向 window.requestAnimationFrame()

  3. requestAnimationFrame 性能更好

  4. 谈谈requestAnimationFrame的动画循环

window.requestIdleCallback


requestIdleCallback 方法只在一帧末尾有空闲的时候,才会执行回调函数;它很适合处理一些需要在浏览器空闲的时候进行处理的任务,比如:统计上传、数据预加载、模板渲染等。

以前如果需要处理复杂的逻辑,不进行分片,用户界面很可能就会出现假死状态,任何的交互操作都将无效;这时使用 setTimeout 就可以把任务拆分成多个模块,每次只处理一个模块,这样能很大程度上缓解这个问题。但是这种方式具有很强的不确定性,我们不知道这一帧是否空闲,如果已经塞满了一大堆任务,这时在处理模块就不太合适了。因此,在这种情况下,我们也可以使用 requestIdleCallback 方法来尽可能高效地利用空闲来处理分片任务。

如果一直没有空闲,requestIdleCallback 就只能永远在等待状态吗?当然不是,它的参数除了回调函数之外,还有一个可选的配置对象,可以使用 timeout 属性设置超时时间;当到达这个时间,requestIdleCallback 的回调就会立即推入事件队列。来看下如何使用:

// 任务队列 const tasks = [

() => {

console.log(“第一个任务”);

},

() => {

console.log(“第二个任务”);

},

() => {

console.log(“第三个任务”);

},

];

// 设置超时时间 const rIC = () => window.requestIdleCallback(runTask, {timeout: 3000})

function work() {

tasks.shift()();

}

function runTask(deadline) {

if (

(

deadline.timeRemaining() > 0 ||

deadline.didTimeout

) &&

tasks.length > 0

) {

work();

}

if (tasks.length > 0) {

rIC();

}

}

rIC();

回调函数参数的详细说明可以查看 MDN 的文档。

改变 DOM


不应该在 requestIdleCallback 方法的回调函数中改变 DOM。我们来看下在某一帧的末尾,回调函数被触发,它在一帧中的位置:

回调函数安排在帧提交之后,也就是说这时渲染已经完成了,布局已经重新计算过;如果我们在回调中改变样式,并且在下一帧中读取布局信息,那之前所作的所有布局计算全都浪费掉了,浏览器会强制重新进行布局计算,这也被称为 强制同步布局。

如果真的想要修改 DOM,那么最佳实践是:在 requestIdleCallback 的回调中构建 Document Fragment,然后在下一帧的 requestAnimationFrame 回调进行真实的 DOM 变动。

Fiber


React 16 推出了新的协调器,Fiber Reconciler(纤维协调器)。它和原先 Stack Reconciler(栈协调器)不同的是:整个渲染过程不是连续不中断完成的;而是进行了分片,分段处理任务,这就需要用到 requestIdleCallbackrequestAnimationFrame 方法来实现。requestIdleCallback 负责低优先级的任务,requestAnimationFrame 负责动画相关的高优先级任务。

参考资料


  1. requestIdleCallback-后台任务调度

  2. 你应该知道的requestIdleCallback

  3. 使用requestIdleCallback

  4. React Fiber初探 —— 调和(Reconciliation)

Web Worker


JavaScript 采用的是单线程模型,也就是说,所有任务都要在一个线程上完成,一次只能执行一个任务。有时,我们需要处理大量的计算逻辑,这是比较耗费时间的,用户界面很有可能会出现假死状态,非常影响用户体验。这时,我们就可以使用 Web Worker 来处理这些计算。

Web Worker 是 HTML5 中定义的规范,它允许 JavaScript 脚本运行在主线程之外的后台线程中。这就为 JavaScript 创造了 多线程 的环境,在主线程,我们可以创建 Worker 线程,并将一些任务分配给它。Worker 线程与主线程同时运行,两者互不干扰。等到 Worker 线程完成任务,就把结果发送给主线程。

Web Worker 与其说创造了多线程环境,不如说是一种回调机制。毕竟 Worker 线程只能用于计算,不能执行更改 DOM 这些操作;它也不能共享内存,没有 线程同步 的概念。

Web Worker 的优点是显而易见的,它可以使主线程能够腾出手来,更好的响应用户的交互操作,而不必被一些计算密集或者高延迟的任务所阻塞。但是,Worker 线程也是比较耗费资源的,因为它一旦创建,就一直运行,不会被用户的操作所中断;所以当任务执行完毕,Worker 线程就应该关闭。

Web Workers API


一个 Worker 线程是由 new 命令调用 Worker() 构造函数创建的;构造函数的参数是:包含执行任务代码的脚本文件,引入脚本文件的 URI 必须遵守同源策略。

Worker 线程与主线程不在同一个全局上下文中,因此会有一些需要注意的地方:

  • 两者不能直接通信,必须通过消息机制来传递数据;并且,数据在这一过程中会被复制,而不是通过 Worker 创建的实例共享。详细介绍可以查阅 worker中数据的接收与发送:详细介绍。

  • 不能使用 DOM、windowparent 这些对象,但是可以使用与主线程全局上下文无关的东西,例如 WebScoketindexedDBnavigator 这些对象,更多能够使用的对象可以查看Web Workers可以使用的函数和类。

使用方式


Web Worker 规范中定义了两种不同类型的线程;一个是 Dedicated Worker(专用线程),它的全局上下文是 DedicatedWorkerGlobalScope 对象;另一个是 Shared Worker(共享线程),它的全局上下文是 SharedWorkerGlobalScope 对象。其中,Dedicated Worker 只能在一个页面使用,而 Shared Worker 则可以被多个页面共享。

下面我来简单介绍一下使用方式,更多的 API 可以查看 使用 Web Workers。

专用线程


下面代码最重要的部分在于两个线程之间怎么发送和接收消息,它们都是使用 postMessage 方法发送消息,使用 onmessage 事件进行监听。区别是:在主线程中,onmessage 事件和 postMessage 方法必须挂载在 Worker 的实例上;而在 Worker 线程,Worker 的实例方法本身就是挂载在全局上下文上的。

Web Worker 专用线程

+

确定

// main.js

const number1 = document.querySelector(“#number1”);

const number2 = document.querySelector(“#number2”);

const button = document.querySelector(“#button”);

const result = document.querySelector(“#result”);

// 1. 指定脚本文件,创建 Worker 的实例

const worker = new Worker(“./worker.js”);

button.addEventListener(“click”, () => {

// 2. 点击按钮,把两个数字发送给 Worker 线程

worker.postMessage([number1.value, number2.value]);

});

// 5. 监听 Worker 线程返回的消息

// 我们知道事件有两种绑定方式,使用 addEventListener 方法和直接挂载到相应的实例

worker.addEventListener(“message”, e => {

result.textContent = e.data;

console.log(“执行完毕”);

})

// worker.js

// 3. 监听主线程发送过来的消息

onmessage = e => {

console.log(“开始后台任务”);

const result= +e.data[0]+ +e.data[1];

console.log(“计算结束”);

// 4. 返回计算结果到主线程

postMessage(result);

}

共享线程


共享线程虽然可以在多个页面共享,但是必须遵守同源策略,也就是说只能在相同协议、主机和端口号的网页使用。

示例基本上与专用线程的类似,区别是:

  • 创建实例的构造器不同。

  • 主线程与共享线程通信,必须通过一个确切打开的端口对象;在传递消息之前,两者都需要通过 onmessage 事件或者显式调用 start 方法打开端口连接。而在专用线程中这一部分是自动执行的。

// main.js

const number1 = document.querySelector(“#number1”);

const number2 = document.querySelector(“#number2”);

const button = document.querySelector(“#button”);

const result = document.querySelector(“#result”);

// 1. 创建共享实例 const worker = new SharedWorker(“./worker.js”);

// 2. 通过端口对象的 start 方法显式打开端口连接,因为下文没有使用 onmessage 事件 worker.port.start();

button.addEventListener(“click”, () => {

// 3. 通过端口对象发送消息   worker.port.postMessage([number1.value, number2.value]);

});

// 8. 监听共享线程返回的结果 worker.port.addEventListener(“message”, e => {

result.textContent = e.data;

console.log(“执行完毕”);

});

// worker.js

// 4. 通过 onconnect 事件监听端口连接 onconnect = function (e) {

// 5. 使用事件对象的 ports 属性,获取端口   const port = e.ports[0];

// 6. 通过端口对象的 onmessage 事件监听主线程发送过来的消息,并隐式打开端口连接   port.onmessage = function (e) {

console.log(“开始后台任务”);

const result= e.data[0] * e.data[1];

console.log(“计算结束”);

console.log(this);

// 7. 通过端口对象返回结果到主线程     port.postMessage(result);

}

}

参考资料


  1. 优化 JavaScript 执行 —— 降低复杂性或使用 Web Worker

  2. 使用 Web Workers

  3. 深入 HTML5 Web Worker 应用实践:多线程编程

  4. JS与多线程

防抖和节流函数


在进行改变窗口大小、滚动网页、输入内容这些操作时,事件回调会十分频繁的被触发,严重增加了浏览器的负担,导致用户体验非常糟糕。此时,我们就可以考虑采用防抖和节流函数来处理这类调动频繁的事件回调,同时它们也不会影响实际的交互效果。

我们先来简单了解一下这两个函数:

  • 防抖(debounce)函数。在持续触发事件时,并不执行事件回调;只有在一段时间之内,没有再触发事件的时候,事件回调才会执行一次。

  • 节流(throttle)函数。在持续触发事件时,事件回调也会不断的间隔一段时间后执行一次。

这两个函数最大的区别在于执行的时机,防抖函数会在事件触发停止一段时间后执行事件回调;而节流函数会在事件触发时不断的间隔一段时间后执行事件回调。我们用定时器来简单实现一下这两个函数,详细版本可以参考 Underscore 和 Lodash —— debounce、Lodash —— throttle。节流函数其实在浏览器拥有 requestAnimationFrame 方法之后,使用这个方法调用事件回调会更好一些。

实现防抖函数


每次执行到 debounce 返回的函数,都先把上一个定时器清理掉,再重新运行一个定时器;等到最后一次执行这个返回的函数的时候,定时器不会被清理,就可以正常等待定时器结束,执行事件回调了。

function debounce(func, wait) {

let timeout = null;

return function run(…args) {

clearTimeout(timeout);

timeout = setTimeout(() => {

func.apply(this, args);

}, wait);

}

};

实现节流函数


在定时器存在的时候,不在重新生成定时器;等到定时器结束,事件回调执行,就把定时器清空;在下一次执行 throttle 返回的函数的时候,再生成定时器,等待下一个事件回调执行。

function throttle(func, wait) {

let timeout = null;

return function run(…args) {

if (!timeout) {

timeout = setTimeout(() => {

timeout = null;

func.apply(this, args);

}, wait);

}

}

}

参考资料


  1. JS的防抖与节流

  2. 使输入处理程序去除抖动

  3. Underscore

  4. Lodash —— debounce

  5. Lodash —— throttle

降低 Style 的复杂性


我们知道 CSS 最重要的组成部分是选择器和声明,所以我会通过这两方面来讲解如何降低 Style 的复杂性。

避免选择器嵌套


我们在 CSSOM Tree 这一节中了解到:嵌套的选择器会从右向左匹配,这是一个递归的过程,而递归是一种比较耗时的操作。更不用说一些 CSS3 的选择器了,它们会需要更多的计算,例如:

.text:nth-child(2n) .strong {

/* styles */

}

为了确定哪些节点应用这个样式,浏览器必须先询问这是拥有 "strong" class 的节点吗?其父节点恰好是偶数的 "text" class 节点吗?如此多的计算过程,都可以通过一个简单的 class 来避免:

.text-even-strong {

/* styles */

}

这么简单的选择器,浏览器只要匹配一次就可以了。为了准确描述网页结构、可复用和代码共享等方面的考虑,我们可以使用 BEM 来协助开发。

BEM(块,元素,修饰符)


BEM 简单来讲就是一种 class 的命名规范,它建议所有元素都有单个类,并且嵌套也能够很好的组织在类中:

.nav {}

.nav__item {}

如果节点需要与其他节点进行区分,就可以加入修饰符来协助开发:

.nav__item–active {}

更为详细的描述和用法可以查看 Get BEM。

使用开销更小的样式


因为屏幕显示效果的不同,所以浏览器渲染每一个样式的开销也会不一样。例如,绘制阴影肯定要比绘制普通背景的时间要长。我们来对比下这两者之间的开销。

性能优化
  • 16
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值