此篇章只做思考,小简暂时没有足够时间去实践,提供大致的思路。
先来总结一下处理方法有哪些:
"惰性加载"(Lazy Loading),也称为懒加载,是一种优化网页或应用加载时间的技术。 在这种策略下,内容只有在需要时才被加载和渲染,通常是指用户滚动到无需立即加载的内容部分时,该部分内容才开始加载。这种方式对于提高页面加载速度、减少初始加载资源和改善用户体验尤为重要。
懒加载的特性:
实现懒加载通常有多种方式,包括但不限于:
Intersection Observer
API来检测元素是否进入可视区域。v-lazy
、React的lazy
和Suspense
等。实现惰性加载时需要考虑的一些最佳实践和潜在问题:
loading="lazy"
属性实现图片和iframe的懒加载,这是一个原生的懒加载支持,简化了实现,并且提供了更好的兼容性和性能。
DOM
操作合并处理是一种优化策略,旨在减少浏览器进行重绘(repaint)和回流(reflow)的次数,通过合并多次DOM操作为单一更新过程以提升页面性能。 这种方法特别重要,因为频繁的、分散的DOM操作会导致浏览器多次重新计算元素的布局和重新渲染界面,这些操作是计算密集型的,会显著影响用户界面的响应性和性能。 说白了,就是尽可能去减少DOM
的操作次数。
使用DocumentFragment:
DocumentFragment
是一个轻量级的DOM节点,可以作为一个临时的容器来存储多个DOM节点。您可以将所有更改应用到DocumentFragment上,然后一次性地将其添加到DOM树中,这种方法只会触发一次回流和重绘。
这个玩意是原生的API支持,详细了解可以参考掘金这一篇博客:juejin.cn/post/695249…
var fragment = document.createDocumentFragment();
for (let i = 0; i < items.length; i++) {
var element = document.createElement('div');
element.textContent = 'Item ' + i;
fragment.appendChild(element);
}
document.body.appendChild(fragment);
最小化直接对DOM的操作:
style.cssText
而不是单独的样式属性,来减少重绘和回流。批量读取后批量写入:
使用requestAnimationFrame:
requestAnimationFrame
确保在浏览器的下一个重绘之前执行DOM更新,这样可以避免不必要的回流和重绘,这也是下面将要提到的“逐帧渲染”requestAnimationFrame(() => {
// 执行DOM更新
});
利用现代前端框架:
虚拟列表(Virtual List)是一种用于高效渲染大量数据项的前端性能优化技术。 当您有成千上万条数据需要在前端列表中展示时,如果直接将所有数据项渲染到DOM中,将会造成显著的性能瓶颈。 虚拟列表技术能够解决这个问题,它的核心思想是仅在给定时间渲染用户可视区域内的数据项,而不是渲染整个列表。
原理可以大致分为下面几点:
伪代码:
class VirtualList {
constructor(container, options) {
this.container = container; // 容器元素,例如一个div
this.itemHeight = options.itemHeight; // 每个列表项的高度
this.renderAhead = options.renderAhead || 0; // 额外渲染的项数,用于更平滑的滚动体验
this.items = options.items; // 完整的数据列表
this.totalHeight = this.items.length * this.itemHeight; // 总高度
this.visibleCount = Math.ceil(this.container.clientHeight / this.itemHeight); // 可见项目数
this.container.addEventListener('scroll', () => this.handleScroll());
this.render();
}
handleScroll() {
this.render(); // 每次滚动时重新渲染
}
render() {
const scrollTop = this.container.scrollTop; // 获取当前滚动位置
const startIndex = Math.floor(scrollTop / this.itemHeight) - this.renderAhead; // 计算开始索引
const endIndex = startIndex + this.visibleCount + this.renderAhead * 2; // 计算结束索引
// 创建一个文档片段,用于合并DOM操作
const fragment = document.createDocumentFragment();
for (let i = Math.max(0, startIndex); i <= Math.min(endIndex, this.items.length - 1); i++) {
const item = this.items[i];
const div = document.createElement('div');
div.style.height = `${this.itemHeight}px`;
div.textContent = `Item ${item}`; // 假设每个项目只是简单的文本
fragment.appendChild(div);
}
// 清空容器,并添加新的项目
this.container.innerHTML = '';
this.container.appendChild(fragment);
// 调整容器高度以匹配总高度
this.container.style.height = `${this.totalHeight}px`;
}
}
// 使用示例:
const list = new VirtualList(document.getElementById('listContainer'), {
itemHeight: 30,
items: Array.from({ length: 10000 }, (_, i) => `Item ${i}`), // 生成大量数据
});
这段代码展示了一个非常基本的虚拟列表实现:
constructor
初始化基本属性和事件监听。handleScroll
方法在容器滚动时触发,用来重新渲染可视区域内的项目。render
方法计算当前应该显示哪些项目,并更新DOM来反映这些更改。注:这只是一个示例实现,实际应用中可能需要考虑更多的细节和优化,例如处理不同高度的项目、优化大量数据的处理、增加更平滑的滚动处理等。
这个其实也可以归并于惰性加载之中。 分批数据加载,也称为分页加载或按需加载,是一种在前端开发中常用的技术,用于优化大量数据的处理和展示。 这种技术允许应用程序逐步加载数据,而不是一次性加载全部数据,从而提升应用的响应速度和用户体验。 比如:滚动加载。
好处:
伪代码:
let currentPage = 1;
const pageSize = 10;
function loadInitialData() {
fetchData(currentPage, pageSize).then(appendDataToView);
}
function loadMoreData() {
currentPage += 1;
fetchData(currentPage, pageSize).then(appendDataToView);
}
function fetchData(page, size) {
// 发起网络请求获取数据
return fetch(`/api/data?page=${page}&size=${size}`).then(response => response.json());
}
function appendDataToView(data) {
// 将获取的数据追加到视图中
data.forEach(item => {
const element = document.createElement('div');
element.textContent = item.content; // 假定数据项有content字段
document.getElementById('dataList').appendChild(element);
});
}
// 假设有一个按钮用于加载更多数据
document.getElementById('loadMoreBtn').addEventListener('click', loadMoreData);
// 初始化加载
loadInitialData();
简化DOM结构是前端性能优化的关键策略之一。
一个精简且有效的DOM结构可以加速页面渲染,提高用户交互响应速度,并减少内存使用。
以下是一些常用的方法来简化DOM结构:
<div>
或<span>
元素用于布局或者样式修饰,这些都是可以优化掉的。使用CSS伪类或更高级的布局技术(如Flexbox或Grid)可以减少这类元素的使用。<table>
,并避免使用表格进行布局,因为表格布局会导致浏览器渲染速度变慢。<article>
、<section>
、<nav>
、<aside>
等),不仅可以使DOM结构更清晰,还有助于提升网站的可访问性和SEO表现。<iframe>
会创建额外的文档环境,增加页面的复杂度。只有在确实需要将外部内容嵌入到页面中时,才使用iframe,并尽量减少其数量。Cache-Control
头,使浏览器缓存静态资源。<script async>
加载非关键脚本,避免阻塞渲染。<script defer>
延迟加载脚本,直到文档解析完成。Web Workers 提供了一种将一段脚本操作运行在后台线程中的能力,这段脚本独立于其他脚本,不会影响页面的性能。使用 Web Workers,你可以执行处理密集型或耗时任务,而不会冻结用户界面。 Web Workers内容较多,我这里只是简单介绍,如果需要详细的资料可以参考其他文章或者去浏览器搜索。 我推荐一篇来自百度某团队的博客:juejin.cn/post/713971…
如果我没记错,Google好像使用这个来实现了一个机器学习库,具体名称我忘记了。
创建一个 Worker:
// 创建一个 Worker,worker.js 是要在 Worker 线程中运行的脚本
var myWorker = new Worker('worker.js');
在 worker.js
中,编写 Worker 线程应该执行的操作:
// 在 worker.js 文件中
self.addEventListener('message', function(e) {
var data = e.data;
// 处理数据
var result = processData(data);
// 将结果发送回主线程
self.postMessage(result);
});
function processData(data) {
// 处理数据的逻辑
return data; // 返回处理后的数据
}
在主线程中与 Worker 交互:
// 向 Worker 发送数据
myWorker.postMessage({ a: 1, b: 2 });
// 接收来自 Worker 的消息
myWorker.addEventListener('message', function(e) {
console.log('收到来自 Worker 的消息:', e.data);
});
通过这种方式,Web Workers 允许开发者将耗时的计算任务移到后台线程,提高应用的响应性和性能。
Web Workers
的大概原理基于浏览器提供的多线程环境,允许开发者在后台并行执行JavaScript
代码,而不会阻塞主线程。
postMessage
方法向 Worker 发送消息,并通过监听 message
事件来接收 Worker 发回的消息。同样,Worker 本身也通过监听 message
事件来接收主线程的消息,并使用 postMessage
来回应。importScripts()
函数,这使得它们可以利用更多的库和工具来执行任务。terminate()
方法即可立即结束 Worker 的执行,而不必等待其自然完成。说到这个,我想起了Node的“多线程”,他的本质也是站在另一个子进程的基础上模拟多线程操作,而本质貌似还是单线程的。
以下论述来自互联网: Node.js 的多线程实现与传统后端语言(如Java或C++)中的多线程有所不同。 Node.js 的设计理念是单线程非阻塞I/O,这使得它在处理大量并发连接时非常高效。 然而,为了充分利用多核CPU,并行处理计算密集型任务,Node.js 提供了一些机制来模拟“多线程”:
child_process
模块创建子进程,这些子进程可以运行Node.js程序或任何其他程序。子进程的运行是独立的,主Node进程可以与之通过IPC(进程间通信)进行通信。这虽然不是传统意义上的多线程,但可以实现在不同核心上并行执行任务。child_process
不同,Worker Threads 允许共享内存(通过 SharedArrayBuffer),在不同的线程执行JavaScript,并且它们运行在相同的Node.js进程中。这使得数据的共享和通信更为高效,但同时也要注意线程安全的问题。虽然 Node.js 提供了这些并行执行代码的机制,但它们与传统后端语言中的多线程(如Java中的线程,C++中的std::thread)在概念和实现上都有所区别。 在Java或C++中,多线程是语言和运行时的内建特性,可以直接创建和管理线程,这些线程共享进程资源。而Node.js的这些特性更多是建立在进程和工作线程的基础上,需要考虑不同进程或线程间的通信和资源共享问题。
Node.js 本身基于单线程的事件循环模型来处理异步操作,这意味着Node.js的主执行线程是单线程的。所谓的“多线程”能力,实际上是通过以下两种主要机制在 Node.js 中模拟实现的:
child_process
模块创建的子进程实际上是在操作系统层面创建了完全独立的进程。每个子进程都有自己的V8实例和独立的执行线程,它们可以并行执行,但是进程间的通信(IPC)需要额外的开销。虽然这些子进程可以实现并行计算,但它们并不共享内存或执行上下文,每个进程都是完全独立的。worker_threads
模块提供了在同一个Node.js进程内部创建多线程的能力。这里的每个 Worker 线程可以执行一个独立的JavaScript文件,共享一定的内存空间(通过 SharedArrayBuffer),并行执行任务。尽管这更接近传统意义上的多线程,每个 Worker 线程还是独立的执行环境,有自己的V8实例和事件循环。总结来说,Node.js 的主应用逻辑运行在一个单独的主线程上,依赖于事件循环处理非阻塞I/O操作。当涉及到 CPU 密集型任务时,Node.js 通过 child processes 或 worker threads 实现了类似多线程的并行处理能力,但这并不改变 Node.js 在核心上是基于单线程事件循环的设计。
这个不必多说,我偷点懒吧,大概就是让用户去主动触发他需要查阅的资源,触发后再去渲染页面,如:点击查看更多。
如果你看过Vue
和React
部分原理实现,那你肯定知道diff
对比这个操作,不了解的话可以搜索一下。
差异更新(Differential Updating)是一种优化策略,用于减少因数据变更导致的不必要的DOM操作,从而提高Web应用的性能。 它主要用在数据驱动的应用中,尤其是当数据频繁变更时。在差异更新中,只有数据改变的部分会触发DOM更新,而不是重新渲染整个DOM树。
那种数据覆盖式更新就是全量更新,全部都需要重新渲染。
活学活用,大量数据的diff
对比可以配合上方的Web Workers
来进一步优化哦!
服务端渲染(Server-Side Rendering,SSR)是一种在服务器上生成完整的页面HTML代码的技术,然后发送到客户端(浏览器),客户端加载这些HTML显示内容,而不需要等待所有JavaScript被下载和执行来呈现页面内容。 也就是后端将HTML代码渲染好给前端,我们的
Vue
和React
是SPA
程序,渲染全是在客户端,内容过多的话加载速度会拖慢卡顿,而且如果数据很大,客户端配置较差,那就更是难搞了。 所以我们直接在服务端就给页面渲染好,这样客户端压力就少了很多,渲染自然也是迅速了,SSR本质是一种负担转移,将客户端压力转到了服务端。 而且SSR是SEO友好的,SPA反之。
Vue
和React
也有自己的SSR框架,分别是Nuxt
和Next
,尤其是Next
非常好用。
其实动画优化包括了逐帧渲染,但是我还是分开来说比较好。
transition
和animation
属性来定义动画,而不是JavaScript的setInterval
或setTimeout
。transform
和opacity
,浏览器可以利用GPU加速渲染,而不是仅依赖CPU。这可以大大提高动画的性能。width
、height
、margin
、top
等。requestAnimationFrame
(rAF): requestAnimationFrame
来控制动画,而不是setInterval
或setTimeout
。requestAnimationFrame
会在浏览器重绘之前执行动画代码,从而确保动画的流畅性。这个其实包含在动画优化内,不过我还是单独来介绍。 逐帧渲染(Frame-by-frame animation)是一种动画技术,其中每一帧都是独立渲染的,这种方式常用于复杂动画的实现,如传统的动画片或高度交互的Web应用动画。 在Web开发中,逐帧渲染通常指通过JavaScript逐帧更新动画状态,这可以通过
requestAnimationFrame
来实现,确保每次浏览器绘制前更新动画帧。
let currentFrame = 0;
const totalFrames = 60; // 假设动画总共有60帧
function updateAnimation() {
// 更新动画状态,这里简单地递增帧数
currentFrame++;
// 在这里更新DOM或Canvas来反映当前帧的动画状态
// 例如,改变一个元素的位置或旋转角度等
updateDOMForCurrentFrame(currentFrame);
// 如果动画未结束,请求下一帧继续更新
if (currentFrame < totalFrames) {
requestAnimationFrame(updateAnimation);
}
}
function updateDOMForCurrentFrame(frame) {
// 根据当前帧更新DOM,这里仅作为示例
const element = document.getElementById('animated-element');
// 假设动画是移动元素,每帧移动1px
element.style.transform = `translateX(${frame}px)`;
}
// 开始动画
requestAnimationFrame(updateAnimation);
在这个示例中:
updateAnimation
函数是每帧执行的函数,它会更新动画的状态,并在每次浏览器重绘之前被调用。updateDOMForCurrentFrame
函数根据当前帧来更新DOM或Canvas。在这个例子中,它简单地将一个元素每帧向右移动1px。requestAnimationFrame(updateAnimation)
开始动画循环。requestAnimationFrame
会在浏览器下一次重绘前调用updateAnimation
函数,从而实现逐帧更新动画。这种逐帧渲染的方式让动画开发者有更大的控制权,可以实现复杂的动画效果,同时确保动画的流畅性。
我们知道,动画和视频其实分解出来都是一帧一帧的画面,熟悉摄影和动画的相关人员可能非常清楚这一点,而帧率就决定了动画的丝滑程度,比如我的IQOO11S,在屏幕刷新帧率60hz的时候,APP打断动画会有肉眼可见的不够平滑过度,但是我调成144hz就非常非常丝滑。
浏览器的动画和渲染也是如此。
逐帧渲染的原理基于逐个计算并渲染每一帧动画的方式,以创建连续的动画效果。在Web环境中,逐帧渲染通常依赖于requestAnimationFrame
(rAF)方法来实现。这里是其背后的一些关键原理:
requestAnimationFrame
调用一个函数来更新动画并在下一次浏览器重绘之前执行。这意味着您的动画帧与浏览器的刷新率(通常是60次/秒,即每16.67毫秒一帧)同步,从而最大化利用每一帧的渲染能力,确保动画平滑。requestAnimationFrame
进行动画意味着浏览器能够优化动画的性能,减少或避免布局抖动(layout thrashing)和不必要的重绘(repaints),因为浏览器知道您的意图是创建动画,并可以为此做出优化。requestAnimationFrame
的回调频率,以节省计算资源和电能。requestAnimationFrame
通常在被调用的函数内部再次调用自己,形成一个递归循环。这允许浏览器在下一个重绘之前再次执行动画更新逻辑,持续推进动画序列。requestAnimationFrame
是与浏览器的刷新率同步的,它可以避免在屏幕刷新之间产生过多的帧,减少资源浪费,并提供流畅的视觉体验。简单来说,尽量的不要阻塞浏览器。 对于异步就不多说了,
JavaScript
是异步玩的非常出色的语言,小简就偷偷懒了。
WebAssembly(通常缩写为Wasm)是一种为网络而生的新型代码格式,它允许在网页上运行与本地代码相近速度的程序。 WebAssembly 设计为与JavaScript协同工作,旨在成为Web开发者的另一种选择,特别是在性能敏感的应用程序中。 对于这个我了解的不多,没有实际使用过,但是我记得他可以将其他语言编译为WebAssembly格式在浏览器中执行,可以获得非常高的处理性能。
虽然WebAssembly通常需要使用支持的编程语言编写后编译,但以下是一个简化的流程概述,没有具体代码但描述了从C到WebAssembly的一般步骤:
WebAssembly现在正逐渐成为Web开发中的一个重要组成部分,提供了一种强大的方法来提升Web应用的性能和能力。
我目前没有使用过这个,单纯了解了一下,所以伪代码我也就不好写了,大家有兴趣可以去查阅其他资料。
GPU加速是指利用图形处理单元(GPU)来加速图形渲染以及非图形计算的过程,以此提高应用程序的性能。 在Web开发领域,GPU加速通常用于加速网页的图形和动画渲染,提供更流畅和响应更快的用户体验。
可以通过某些CSS属性来提示浏览器使用GPU加速特定元素的渲染:
transform: translateZ(0)
或transform: translate3d(0, 0, 0)
应用于元素,可以创建一个新的合成层,即使这个变换本身没有视觉上的变化。will-change
属性可以用来告知浏览器某个元素预计会有变化(如动画),浏览器可以提前进行优化。开年第一篇结束,先就说到这吧,打字手都打麻了,如果可以的话,麻烦给我点个赞吧,如果需要转载记得署名小简然后备注来源哦!原创不易,感谢支持。
对了,推荐两篇关于瀑布流的大量数据渲染优化方案: