最近,我学习到了一些关于性能优化的问题,性能优化是软件工程中的关键组成部分,它会直接影响到应用程序的效率、响应速度、资源消耗以及最终用户的体验。我整理了最近所学到的关于性能优化的知识写成了这篇文章,如有不正确之处还请大佬指出
了解性能优化
性能优化就像是让你的电脑或者手机变得更聪明、更快、更省电的魔法。想象一下,当你打开一个游戏或者应用程序,你肯定希望它能瞬间加载完成,玩起来流畅不卡顿,同时电池也不至于很快就耗光。这就是性能优化要解决的问题。
就好比你开了一家餐厅,顾客进店后,你希望他们能很快坐下点餐,服务员能迅速上菜,顾客吃完满意离开,而不是让客人等半天才上菜,或者店里一团乱,让顾客感到不爽。在软件的世界里,“顾客”就是用户,“服务员”和“厨师”就是软件的各种功能和后台程序。
性能优化就像给餐厅做改进,比如:
- 让服务员(软件功能)动作更快,减少等待时间;
- 厨房(服务器)布局更合理,减少厨师(程序)走动距离,提高出菜速度;
- 合理安排桌椅(内存和硬盘空间),确保每位顾客(用户请求)都有合适的位置;
- 优化菜单(代码),去掉复杂又不常用的菜品(功能),让厨房(处理器)更专注于热门菜品(常用功能)的制作。
最终目的都是为了让顾客(用户)有更好的体验,让他们觉得这家餐厅(软件)值得再来,愿意推荐给朋友。同样,性能优化好的软件,用户用起来舒心,自然会更受欢迎,口碑也会更好。
所以,性能优化就像是给软件做“健身”,让它变得更健康、更强壮,这样才能更好地服务用户,让用户满意。
我从浏览器的底层机制出发,我认为只有先去了解浏览器是如何工作的,才能更好的去做好性能优化
一、理解浏览器内核:渲染引擎与JS引擎
浏览器内核是驱动Web页面呈现和交互的核心组件,通常由两大部分组成:渲染引擎和JS引擎。
- 渲染引擎:负责解析HTML和CSS,构建DOM树和CSSOM树,并生成渲染树。随后,计算图层布局,进行像素级别的绘制,最终整合图层,将静态或动态页面呈现在屏幕上。
Web浏览器渲染引擎工作流程详解
Web浏览器的渲染引擎是网页显示过程中的核心组件,它负责解析HTML、CSS和JavaScript,构建DOM树和CSSOM树,生成渲染树,并最终绘制页面。下面我们将深入探讨渲染引擎的各个关键步骤,以及每个步骤是如何协同工作以高效地展示Web内容的。
1. HTML解析器:构建DOM树
当浏览器接收到HTML文件后,HTML解析器开始解析文档,逐步构建DOM树(Document Object Model)。DOM树是一个表示文档结构的树形数据结构,其中每个节点代表一个HTML元素。HTML解析器边读取边构建,这意味着它并不等待整个文档加载完成就开始构建DOM树,这种渐进式解析方式可以加快页面的首次显示速度。
2. CSS解析器:生成CSSOM树
与此同时,CSS解析器开始解析CSS文件,生成CSSOM树(CSS Object Model)。CSSOM树是CSS规则的集合,包含了所有应用于当前页面的样式规则。CSSOM树的构建同样采用增量式的方式,即在解析过程中逐渐完善,以便尽早应用样式规则。
3. DOM树与CSSOM树结合:创建渲染树
一旦DOM树和CSSOM树准备就绪,渲染引擎会将它们结合起来,创建渲染树(Render Tree)。渲染树包含所有可见元素的信息,包括它们的样式属性。需要注意的是,非可视元素(如<script>
标签)不会出现在渲染树中。
4. 图层布局计算模块:生成Layout Tree
接下来,图层布局计算模块(Layout Engine)接收渲染树作为输入,计算出每个元素的确切位置和大小,形成Layout Tree(布局树)。这个阶段涉及到复杂的计算,如元素的大小、位置、边距、填充等,以及处理CSS中的盒模型、定位和流布局等。
5. 视图绘制模块:像素级绘制
最后,视图绘制模块(Painting Engine)根据Layout Tree中的信息,将每个元素以像素级精度绘制到屏幕上。这个过程实际上是由CPU计算出每一像素的颜色,然后传递给GPU进行实际的屏幕绘制。GPU的并行处理能力使其在像素绘制上非常高效,能够快速地将计算结果转换为用户可见的画面。
渲染引擎是无需等待JS执行的
怎么样来理解无需等待JS执行呢 给大家一段代码思考一下 两个log会输出啥
<style>
#container {
width: 200px;
height: 200px;
background-color: #eee;
}
</style>
<script>
var container = document.getElementById('container');
console.log("before", container);
</script>
</head>
<body>
<div id="container"></div>
<script>
var container = document.getElementById('container');
console.log("container background",getComputedStyle(container).backgroundColor);
<script>
<style>
#container {
background-color: blue;
}
</style>
</body>
没错,在body标签前的log输出的是 “before”,null
,而下面的则是输出"container background",rgb(238,238,238)
,并不是下面的蓝色,但是页面是蓝色的。由此可见 script会阻塞代码,并且他会影响到性能,所以渲染引擎不能去等待JS代码的执行,那该怎么解决呢
解决方案:
- 把script标签放在最后加载
- 新建一个js文件把js代码写在js文件里然后使用
defer
关键字
<script src="demo.js" defer></script>
- 使用
async
关键字
二、性能优化策略
- JS引擎:执行JavaScript代码,动态修改DOM,触发重绘和重排,从而实现页面的动态效果和交互功能。
大家先来看看这段代码
for (var count = 0; count < 10000; count++) {
let container = document.getElementById('container')
container.innerHTML += `<span>测试</span>`
}
这段代码有什么问题呢
- 首先是性能问题:直接操作
innerHTML
属性在大量迭代中可能引起性能问题,尤其是在像这个例子中连续追加10000个元素的情况下。这是因为每次修改innerHTML
,浏览器都需要重新构建DOM树和重新布局页面,这可能导致性能瓶颈。对于这种场景,更高效的方法是先构建一个文档片段或使用模板字符串拼接,最后一次性更新innerHTML
。 - 重绘与重排:每次
innerHTML
更新,浏览器都可能触发重绘(repainting)和重排(reflow)。重排尤其耗时,因为它涉及到重新计算布局和位置信息。频繁的重排会影响页面的渲染性能,特别是在移动设备上。
那么该如何优化呢?
优化方案
我们先来一个最容易想到的方案
let container = document.getElementById('container');
let content = ''
for (var count = 0; count < 10000; count++) {
content += `<span>测试</span>`
}
container.innerHTML = content
这段代码是直接用一个变量储存10000次的字符串拼接,然后一次性放在div里。但其实性能也不是很好,那么该如何做呢,我们可以使用文档碎片来解决这个问题,详情如下
documentFragmrnt
DocumentFragment
(文档碎片)是一种特殊的DOM节点,它没有出现在文档的实际结构中,但可以像普通DOM节点那样接收子节点。这意味着你可以在DocumentFragment
中构建一系列的DOM元素,然后一次性将其添加到实际的DOM树中,从而减少页面的重排(reflow)次数,提高性能。
在上面的代码示例中,我们首先获取了页面上的一个容器元素(<div id="container">
),然后创建了一个DocumentFragment
实例(let content = document.createDocumentFragment();
)。接下来,在循环中创建了10000个<span>
元素,将它们逐一添加到DocumentFragment
中。
for (var count = 0; count < 10000; count++) {
let span = document.createElement('span');
span.innerHTML = "测试";
content.appendChild(span);
}
当所有<span>
元素都添加完毕后,我们可以将整个DocumentFragment
一次性地追加到container
元素中:
container.appendChild(content);
相比于直接将每个新创建的<span>
元素分别添加到container
中,这种方法能够显著减少DOM操作引起的页面重排次数,从而提高页面性能。这是因为,只有在DocumentFragment
被添加到DOM树中的那一刻,浏览器才需要重新计算布局,而不是每次添加一个元素时都进行计算。
这种方法特别适用于需要动态生成大量DOM元素的情况,例如列表、网格视图或动态加载的内容模块。通过使用DocumentFragment
,你可以确保页面的渲染效率,同时保持代码的清晰和可维护性。
减少重绘(Repaint)
重绘发生在元素的视觉属性发生变化时,如颜色、背景色等,但不包括尺寸和位置。优化策略包括:
-
合并重绘:尽量将多次样式修改合并为一次,减少对渲染树的更新频率。 例如:
container.style.width = '100px'; container.style.height = '200px'; container.style.backgroundColor = 'green';
我们可以把上述代码改成 把这三写在css里 然后给这个节点加上一个类名就可以了
container.classList.add = 'show';
-
减少DOM操作:
- 使用
DocumentFragment
作为临时容器,在其中完成所有DOM元素的创建和修改,最后一次性插入文档中。 - 利用
cloneNode
复制已存在的DOM节点,减少新节点的创建开销。 - 更新元素内容时,优先考虑使用
innerHTML
或innerText
,它们比逐个元素操作更高效。
- 使用
减少重排(Reflow)
重排发生在元素的几何尺寸或位置发生变化时 如:container.style.width = '100px' container.style.height = '200px';
,这不仅影响自身,还可能影响周围元素。优化策略包括:
- 使用CSS变换:使用
transform
属性替代top
、left
等定位属性,因为变换不会触发重排。 - 使用
visibility
而非display
:visibility: hidden
仅隐藏元素而不改变布局,而display: none
会触发重排。 - 使用
classList
替换className
:classList
提供了更安全且高效的类名操作。 - 利用CSS3动画和过渡:相比JavaScript,CSS3动画和过渡更加流畅,且对页面布局的影响较小。
- 选择CSS3选择器和计算能力:现代浏览器的CSS引擎在处理选择器和计算上通常比JavaScript更快。
页面绘制流程优化
网页的绘制流程包括解析HTML、计算样式、布局、绘制和合成。优化点在于:
- 避免昂贵的CSS选择器:避免使用过多嵌套的选择器,关注那些可以通过继承实现的属性。
- 减少通配符使用:
*
选择器会导致全局搜索,非常耗时。 - 精简选择器:尽量使用ID选择器或类选择器,避免过于复杂的选择器组合。
防抖与节流
防抖和节流是两种常见的事件处理策略,用于控制事件处理器的执行频率,减轻服务器压力,提高用户体验。
- 防抖:连续触发的事件,在一定时间内只执行最后一次。例如,在搜索框中输入文字时,只有在用户停止输入一定时间后才发送查询请求。
实现方案:
function debounce(func, delay) {
// 返回值必须得是函数 keyup 事件处理函数
return function (args) {
clearTimeout(func.id); // 清除定时器
// 对象 , id挂载到func对象上 func是闭包中的自由变量
func.id = setTimeout(() => func(args), delay);
};
}
- 节流:保证在一定时间内只执行一次事件处理器。这对于需要频繁响应的事件(如滚动或调整窗口大小)非常有用,可以防止处理器过于频繁地执行。
实现方案:
const throttle = (func, delay) => {
// 才是事件的处理函数
// 定义时,生成时 func delay
// keyup return func 调用时能找到闭包的自由变量
// last 用于记录上次触发的时间 deferTimer 用于定时器id
let last, deferTimer; // 自由变量
return (...args) => {
// 当前时间,隐式类型转换
let now = +new Date()
if (last && now - last < delay) {
clearTimeout(deferTimer);
deferTimer = setTimeout(() => {
last = now;
func(...args);
}, delay);
}else{
last = now;
func(...args);
}
}
}
三、事件与API设计
在构建Web应用时,合理设计事件处理器和API接口也是提高性能的关键。
- 事件设计:使用如
keyup
等事件时,结合防抖或节流策略,避免过度响应用户的输入行为,减轻服务器负担。 - RESTful API:遵循RESTful原则设计API,使得资源访问方式清晰且一致,有助于优化网络请求,减少不必要的数据传输。
四、ES6特性与模块化
ES6(ECMAScript 2015)是JavaScript的一个重要版本,引入了许多新特性和语法糖,极大地提升了语言的表达能力和开发效率。以下是ES6一些关键特性的总结,分为简洁优雅、可读性增强和大型语言能力三方面:
简洁优雅
-
箭头函数:箭头函数简化了函数表达式的书写,避免了在函数内部使用this关键字的复杂性。这可以减少函数调用的开销,尤其是在创建大量匿名函数时,因为箭头函数不需要创建自己的this上下文。
const add = (a, b) => a + b;
-
展开运算符:用于将数组或对象的元素拆分或合并,提高了代码的灵活性。
const arr1 = [1, 2], arr2 = [3, 4]; const combined = [...arr1, ...arr2];
-
解构赋值:解构赋值允许从数组或对象中快速提取值,这比传统的属性访问或索引访问更快,尤其在处理复杂的嵌套结构时,解构赋值可以显著减少代码量,提高代码的执行效率。
const { name, age } = user;
-
字符串模板:字符串模板通过反引号和${}插值,使得字符串拼接更加直观和高效,减少了传统字符串拼接时的运行时开销,尤其是在循环中拼接大量字符串时,性能优势更为明显。
const greeting = `Hello, ${name}!`;
-
Promise:Promise替代了传统的回调函数,通过链式调用.then()处理异步操作,这不仅提高了代码的可读性,也避免了“回调地狱”的复杂性,从而间接提高了程序的性能。
fetch(url).then(response => response.json()).then(data => console.log(data));
五、总结
构建高性能Web应用是一项综合艺术,涉及到对浏览器底层原理的理解、合理的设计模式、先进的编程技术和细致的性能调优。通过上述讨论,我们了解到渲染引擎和JS引擎的工作机制,掌握了减少重绘与重排、优化CSS选择器、应用防抖和节流等策略,以及ES6特性和模块化的运用。更重要的是,我们认识到,性能优化是一个持续的过程,需要开发者不断学习新技术、新方法,以适应不断变化的Web环境,为用户提供更加流畅、高效的Web体验。 。