# 从 15 个点来思考前端大量数据渲染与频繁更新的方案

先来总结一下处理方法有哪些:

  1. 惰性加载 (懒加载)
  2. DOM 操作合并处理
  3. 虚拟列表
  4. 分批数据加载
  5. 简化 DOM 结构
  6. 优化资源
  7. Web Workers
  8. 用户操作优化
  9. 差异更新
  10. 服务端渲染
  11. 动画优化
  12. 逐帧渲染
  13. 异步更新
  14. WebAssembly
  15. GPU 加速

# 惰性加载

# 介绍

"惰性加载"(Lazy Loading),也称为懒加载,是一种优化网页或应用加载时间的技术。

在这种策略下,内容只有在需要时才被加载和渲染,通常是指用户滚动到无需立即加载的内容部分时,该部分内容才开始加载。这种方式对于提高页面加载速度、减少初始加载资源和改善用户体验尤为重要。

懒加载的特性:

  1. 减少初始加载时间:通过推迟加载页面上非关键资源的加载(如图片、视频、广告、不可见内容等),页面的首次加载时间可以显著减少,用户可以更快地看到和交互的页面内容。
  2. 节省资源只加载用户可以看到的内容,可以节省带宽和服务器资源,对于用户和服务器都是有益的,尤其是在流量费用昂贵或网络连接不佳的情况下。
  3. 提升用户体验:用户无需等待所有元素加载完毕即可使用网站,从而减少跳出率,提高用户满意度和参与度。
  4. SEO 优化:虽然懒加载对 SEO 有潜在的负面影响,因为搜索引擎的爬虫可能无法加载和索引懒加载的内容,但通过适当的实现和优化,比如使用 Intersection Observer API,确保内容在爬虫访问时能够被加载,可以减少这种影响。

# 实现

实现懒加载通常有多种方式,包括但不限于:

  • 使用 Intersection Observer API 来检测元素是否进入可视区域。
  • 基于滚动事件,结合元素的位置信息来判断是否需要加载。
  • 使用现代前端框架提供的懒加载组件或指令,如 Vue 的 v-lazy 、React 的 lazySuspense 等。

# 扩展

实现惰性加载时需要考虑的一些最佳实践和潜在问题:

  1. 预加载关键资源:虽然懒加载推迟了非关键资源的加载,但对于关键资源,如页面首屏可见内容的关键图片或数据,应确保它们能够尽快加载,以避免用户看到不完整或空白的页面。
  2. 占位符的使用:在资源被加载之前,可以使用适当的占位符(如加载动画、低质量图像预览等)来提供更好的用户体验,防止页面布局突然变化导致的用户困扰。
  3. 无障碍性(Accessibility):确保懒加载实现不会破坏网站的无障碍性。例如,对于视觉障碍用户使用的屏幕阅读器,需要确保懒加载的内容在被访问时能够正确加载和宣读。
  4. 测试和验证:在不同的设备和网络环境下测试懒加载的实现,确保在所有情况下都能正常工作,特别是在低端设备和慢速网络环境下。
  5. 与现代浏览器特性结合:例如,利用 loading="lazy" 属性实现图片和 iframe 的懒加载,这是一个原生的懒加载支持,简化了实现,并且提供了更好的兼容性和性能。
  6. 监控性能影响:使用性能监测工具(如 Google Lighthouse)来评估懒加载实现对网站性能的影响,确保优化目标得到实现,并调整策略以解决任何潜在的性能问题。

# DOM 操作合并处理

# 介绍

DOM 操作合并处理是一种优化策略,旨在减少浏览器进行重绘(repaint)和回流(reflow)的次数,通过合并多次 DOM 操作为单一更新过程以提升页面性能。

这种方法特别重要,因为频繁的、分散的 DOM 操作会导致浏览器多次重新计算元素的布局和重新渲染界面,这些操作是计算密集型的,会显著影响用户界面的响应性和性能。

说白了,就是尽可能去减少 DOM 的操作次数

# 实现

  1. 使用 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);
  2. 最小化直接对 DOM 的操作:

    • 尽量减少直接对 DOM 的修改次数,如果需要应用多个更改,可以先计算出最终状态,然后应用这些更改,而不是逐一更改
    • 对于样式更改,可以通过修改类名或 style.cssText 而不是单独的样式属性,来减少重绘和回流。
  3. 批量读取后批量写入:

    • 浏览器会对 DOM 的连续读取和写入进行优化。如果你交替进行读写操作,浏览器可能需要多次回流,推荐的方法是先进行所有的 DOM 读取操作,然后再进行所有的 DOM 写入操作,或者服务端直接返回渲染好的 HTML 字符串
  4. 使用 requestAnimationFrame:

    • 对于需要频繁更新 DOM 的场景,如动画或在滚动事件中更新元素,使用 requestAnimationFrame 确保在浏览器的下一个重绘之前执行 DOM 更新,这样可以避免不必要的回流和重绘,这也是下面将要提到的 “逐帧渲染
    requestAnimationFrame(() => {
      // 执行 DOM 更新
    });
  5. 利用现代前端框架:

    • 现代前端框架(如 React、Vue、Angular)内部已经实现了虚拟 DOM,它们可以在内存中计算出最小的 DOM 变更集,然后应用这些变更,这样可以大大减少直接的 DOM 操作,提升性能。

# 虚拟列表

# 介绍

虚拟列表(Virtual List)是一种用于高效渲染大量数据项的前端性能优化技术。

当您有成千上万条数据需要在前端列表中展示时,如果直接将所有数据项渲染到 DOM 中,将会造成显著的性能瓶颈

虚拟列表技术能够解决这个问题,它的核心思想是仅在给定时间渲染用户可视区域内的数据项,而不是渲染整个列表。

原理可以大致分为下面几点:

  1. 渲染可视项:虚拟列表只渲染进入用户可视范围内的项目,当用户滚动列表时,组件计算当前可视范围,并只渲染这个范围内的项目。

    想象你在一家大型图书馆里,有成千上万的书籍。

    但你的视线所及之处,只能看到几十本书。

    虚拟列表就像图书馆管理员,当你站在图书馆的某个位置时,管理员只给你拿来那部分你能看到的书,而不是整个图书馆的所有书。

    当你走到图书馆的另一部分时,管理员会根据你的新位置再次给你拿来那一区域的书。

    这样,无论图书馆有多少书,管理员都只需要管理你当前可以看到的那些书。

  2. 回收和重用 DOM:当数据项滚动出视图时,虚拟列表会回收这些项的 DOM 元素,并在新的可视数据项进入视图时重用这些 DOM 元素,这样可以大大减少 DOM 操作的数量。

    延续上面的比喻,当你从图书馆的一部分走到另一部分时,你不可能同时看两个地方的书。

    图书馆管理员会把你不再需要的书放回原位,然后把新区域的书拿给你。

    在虚拟列表中,"放回原位" 相当于回收 DOM 元素,而 "拿新书" 则相当于重用 DOM 元素。

    这样做减少了总体上需要管理的书籍(DOM 元素)数量,从而提高效率。

  3. 动态计算:虚拟列表组件会动态计算并调整滚动容器的滚动高度,以确保滚动行为与真实的数据量相匹配,为用户提供准确的滚动体验。

    如果图书馆的书架是可移动的,并且管理员根据你想要的书的位置调整书架的高度,使你总是感觉到所有书就在你的可达范围内,那么这个过程就类似于虚拟列表的动态计算。

    虚拟列表会计算当前应该显示内容的正确大小和位置,调整滚动容器的高度,使得滚动行为看起来和感觉上就像是在处理全部数据,虽然实际上只渲染了一部分内容。

# 优势

  1. 性能提升:通过减少渲染的 DOM 数量,虚拟列表大幅降低了浏览器的负担,提升了渲染性能,尤其是在处理大量数据时。
  2. 响应速度快:用户滚动列表时,界面能够快速响应,因为只需要处理和渲染少量的数据项。
  3. 内存使用优化:减少在 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 来反映这些更改。

注:这只是一个示例实现,实际应用中可能需要考虑更多的细节和优化,例如处理不同高度的项目、优化大量数据的处理、增加更平滑的滚动处理等。

# 分批加载

# 介绍

这个其实也可以归并于惰性加载之中。

分批数据加载,也称为分页加载或按需加载,是一种在前端开发中常用的技术,用于优化大量数据的处理和展示。

这种技术允许应用程序逐步加载数据,而不是一次性加载全部数据,从而提升应用的响应速度和用户体验。

比如:滚动加载

  • 初始加载少量数据:当用户首次访问应用时,只加载一小部分数据(例如,列表的第一页或前几项数据)。
  • 按需加载更多数据:随着用户的交互(如滚动到列表底部或点击 “加载更多” 按钮),应用逐步加载更多数据。

好处:

  1. 提高性能:减少初始加载的数据量,加快应用加载和响应速度。
  2. 减少资源消耗:按需加载数据减少了服务器的压力和网络资源的消耗。
  3. 改善用户体验:用户不需要等待全部数据加载完成即可开始浏览,提升了用户体验。

# 实现

  1. 后端支持:确保后端 API 支持分页或分批获取数据,通常需要提供如页码(page)和每页数量(pageSize)等参数。
  2. 前端请求数据:前端在需要时发送请求获取数据,传递相应的分页参数。
  3. 用户触发加载:根据用户行为(如滚动、点击等)来触发更多数据的加载。
  4. 更新前端视图:将加载的新数据追加到当前数据列表的末尾,并更新视图。

伪代码:

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 结构可以加速页面渲染,提高用户交互响应速度,并减少内存使用。

以下是一些常用的方法来简化 DOM 结构:

  1. 避免深层嵌套:过深的 DOM 结构会影响页面的渲染效率。应尽量避免不必要的层级嵌套,例如,可以用较少的包装元素实现相同的布局和样式。
  2. 移除无用的包装元素:经常可以看到一些空的或者没有实际作用的 <div><span> 元素用于布局或者样式修饰,这些都是可以优化掉的。使用 CSS 伪类或更高级的布局技术(如 Flexbox 或 Grid)可以减少这类元素的使用。
  3. 利用 CSS 代替空的或纯布局的 DOM 元素:很多时候,我们可以通过 CSS 的能力(如伪元素、边框、阴影、布局模型等)来代替那些仅用于视觉表现的 DOM 元素。
  4. 合理使用表格:仅当呈现表格数据时使用 <table> ,并避免使用表格进行布局,因为表格布局会导致浏览器渲染速度变慢。
  5. 优化动态生成的内容:对于通过 JavaScript 动态生成并添加到页面的内容,应注意控制生成的 DOM 元素数量和复杂度,避免在每次更新时重建整个结构。
  6. 使用语义化的 HTML:合理使用 HTML5 提供的语义化标签(如 <article><section><nav><aside> 等),不仅可以使 DOM 结构更清晰,还有助于提升网站的可访问性和 SEO 表现。
  7. 减少 iframe 的使用<iframe> 会创建额外的文档环境,增加页面的复杂度。只有在确实需要将外部内容嵌入到页面中时,才使用 iframe,并尽量减少其数量。

# 资源优化

  1. 资源压缩:
    1. CSS 压缩:移除所有多余的空格、注释,缩短 CSS 类名和 ID,使用在线工具如 CSSMinifier。
    2. JavaScript 压缩:删除不必要的字符、注释,使用在线工具如 UglifyJS 或 Terser 进行压缩。
    3. 图片压缩:使用工具如 TinyPNG 或 ImageOptim 减小图片文件尺寸,无损压缩或适量有损压缩。
  2. 资源合并:
    1. CSS 合并:将多个 CSS 文件合并为一个文件,减少 HTTP 请求次数。
    2. JavaScript 合并:类似地,将多个 JavaScript 文件合并,以减少请求。
  3. 缓存利用:
    1. 浏览器缓存:通过设置合适的 Cache-Control 头,使浏览器缓存静态资源。
    2. 服务端缓存:配置服务器缓存策略,如 ETag 或 Last-Modified 头,优化资源的重新请求。
  4. 异步加载:
    1. 异步脚本:使用 <script async> 加载非关键脚本,避免阻塞渲染。
    2. 延迟脚本:使用 <script defer> 延迟加载脚本,直到文档解析完成。
  5. CDN 使用:
    1. 静态资源分发:使用 CDN 分发静态资源,如图片、CSS、JavaScript,接近用户地理位置的服务器提供数据,减少延迟。
    2. 内容缓存:利用 CDN 的缓存策略,提高资源访问速度。
  6. 字体优化:
    1. 字体子集化:只包含网页所需的字符,减少字体文件大小。
    2. 格式选择:优先使用 WOFF2 格式,兼顾压缩效率和兼容性。
  7. 懒加载实现:
    1. 图片懒加载:当图片进入视口时才加载,可以使用 Intersection Observer API 实现。
    2. iframe 懒加载:同样,延迟加载不立即需要的 iframe 内容。
  8. 关键 CSS 优化:
    1. 内联关键 CSS:将关键渲染路径上的 CSS 内联到 HTML 中,加速首次渲染。
    2. 避免阻塞渲染:确保加载非关键 CSS 不会阻塞页面渲染。
  9. 现代格式应用:
    1. 图片格式:使用 WebP 或 AVIF 格式替代传统的 JPEG 和 PNG,优化质量与大小的平衡。
    2. 视频格式:对于视频,使用如 H.264 或 VP9 的现代编码技术。
  10. 性能监控:
    1. 使用 Lighthouse:定期使用 Google Lighthouse 进行性能审查,获取优化建议。
    2. 应用 WebPageTest:使用 WebPageTest 进行更深入的性能分析和监测。

# Web Workers

# 介绍

Web Workers 提供了一种将一段脚本操作运行在后台线程中的能力,这段脚本独立于其他脚本,不会影响页面的性能。使用 Web Workers,你可以执行处理密集型或耗时任务,而不会冻结用户界面。

Web Workers 内容较多,我这里只是简单介绍,如果需要详细的资料可以参考其他文章或者去浏览器搜索。

我推荐一篇来自百度某团队的博客:juejin.cn/post/713971…

# 特点

  1. 并行执行:Web Workers 运行在与主线程分离的后台线程中,允许进行并行计算,而不会阻塞或减慢用户界面。
  2. 独立运行:Workers 在独立的全局上下文中运行,不会影响主页面的性能。
  3. 数据交互:主线程和 Workers 之间可以通过传递消息的方式交换数据,这些消息在传输过程中会被复制,而不是共享。
  4. 限制:Web Workers 不能访问 DOM 节点,也不能使用 window 或 document 对象的方法。它们主要用于执行与 UI 无关的计算密集型或耗时任务。

# 场景

  1. 图像处理:在图像编辑应用中,Web Workers 可用于执行复杂的图像处理算法,而不会导致界面卡顿。
  2. 大数据计算:在需要处理大量数据的应用中,例如分析或计算密集型任务,Web Workers 可以在后台进行,不影响前端的响应。
  3. 实时数据处理:对于需要实时处理数据的应用,如游戏或交互式图形,Web Workers 可以在后台执行数据处理,提供流畅的用户体验。

如果我没记错,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 代码,而不会阻塞主线程

  1. 线程隔离:Web Workers 运行在与主线程完全隔离的后台线程中。这意味着它们有自己的执行上下文和全局作用域,不能直接访问主线程中的全局变量或 UI。
  2. 消息传递机制:主线程与 Worker 之间的通信基于消息传递机制。这些消息在传递过程中不是直接共享,而是通过结构化克隆算法被复制。这种机制确保了数据的完整性和线程安全。
  3. 事件驱动:Worker 与主线程之间的通信是基于事件的。主线程使用 postMessage 方法向 Worker 发送消息,并通过监听 message 事件来接收 Worker 发回的消息。同样,Worker 本身也通过监听 message 事件来接收主线程的消息,并使用 postMessage 来回应。
  4. 执行环境:尽管 Workers 不能直接操作 DOM,它们可以执行几乎所有的 JavaScript 代码,包括计时器、使用 XHR 请求(尽管存在一些限制)、使用 Fetch API 等。
  5. 资源加载:Workers 可以导入外部脚本使用 importScripts() 函数,这使得它们可以利用更多的库和工具来执行任务。
  6. 生命周期控制:主线程可以随时创建和终止 Workers,使用 terminate() 方法即可立即结束 Worker 的执行,而不必等待其自然完成。
  7. 独立性能影响:由于 Worker 在后台线程中执行,因此即使执行密集计算也不会阻塞 UI 渲染或用户交互,从而提升了应用的响应性和性能。

# 扩展

说到这个,我想起了 Node 的 “多线程”,他的本质也是站在另一个子进程的基础上模拟多线程操作,而本质貌似还是单线程的。

以下论述来自互联网:

Node.js 的多线程实现与传统后端语言(如 Java 或 C++)中的多线程有所不同。

Node.js 的设计理念是单线程非阻塞 I/O,这使得它在处理大量并发连接时非常高效。

然而,为了充分利用多核 CPU,并行处理计算密集型任务,Node.js 提供了一些机制来模拟 “多线程”:

  1. Child Processes:
    • Node.js 可以通过 child_process 模块创建子进程,这些子进程可以运行 Node.js 程序或任何其他程序。子进程的运行是独立的,主 Node 进程可以与之通过 IPC(进程间通信)进行通信。这虽然不是传统意义上的多线程,但可以实现在不同核心上并行执行任务。
  2. Cluster 模块:
    • Cluster 模块允许创建多个 Node.js 进程(称为工作进程)。主进程(主线程)可以管理这些工作进程,并将入站连接分发给它们,实现负载均衡。每个工作进程都是独立的,运行在自己的 V8 实例中,有自己的事件循环。
  3. Worker Threads:
    • Node.js 12 引入的 Worker Threads 提供了更接近传统多线程的功能。与 child_process 不同,Worker Threads 允许共享内存(通过 SharedArrayBuffer),在不同的线程执行 JavaScript,并且它们运行在相同的 Node.js 进程中。这使得数据的共享和通信更为高效,但同时也要注意线程安全的问题。

虽然 Node.js 提供了这些并行执行代码的机制,但它们与传统后端语言中的多线程(如 Java 中的线程,C++ 中的 std::thread)在概念和实现上都有所区别。

在 Java 或 C++ 中,多线程是语言和运行时的内建特性,可以直接创建和管理线程,这些线程共享进程资源。而 Node.js 的这些特性更多是建立在进程和工作线程的基础上,需要考虑不同进程或线程间的通信和资源共享问题。

Node.js 本身基于单线程的事件循环模型来处理异步操作,这意味着 Node.js 的主执行线程是单线程的。所谓的 “多线程” 能力,实际上是通过以下两种主要机制在 Node.js 中模拟实现的:

  1. Child Processes:
    • 通过 child_process 模块创建的子进程实际上是在操作系统层面创建了完全独立的进程。每个子进程都有自己的 V8 实例和独立的执行线程,它们可以并行执行,但是进程间的通信(IPC)需要额外的开销。虽然这些子进程可以实现并行计算,但它们并不共享内存或执行上下文,每个进程都是完全独立的。
  2. Worker Threads:
    • worker_threads 模块提供了在同一个 Node.js 进程内部创建多线程的能力。这里的每个 Worker 线程可以执行一个独立的 JavaScript 文件,共享一定的内存空间(通过 SharedArrayBuffer),并行执行任务。尽管这更接近传统意义上的多线程,每个 Worker 线程还是独立的执行环境,有自己的 V8 实例和事件循环。

总结来说,Node.js 的主应用逻辑运行在一个单独的主线程上,依赖于事件循环处理非阻塞 I/O 操作。当涉及到 CPU 密集型任务时,Node.js 通过 child processes 或 worker threads 实现了类似多线程的并行处理能力,但这并不改变 Node.js 在核心上是基于单线程事件循环的设计。

# 用户操作优化

这个不必多说,我偷点懒吧,大概就是让用户去主动触发他需要查阅的资源,触发后再去渲染页面,如:点击查看更多。

# 差异更新

如果你看过 VueReact 部分原理实现,那你肯定知道 diff 对比这个操作,不了解的话可以搜索一下。

# 介绍

差异更新(Differential Updating)是一种优化策略,用于减少因数据变更导致的不必要的 DOM 操作,从而提高 Web 应用的性能。

它主要用在数据驱动的应用中,尤其是当数据频繁变更时。在差异更新中,只有数据改变的部分会触发 DOM 更新,而不是重新渲染整个 DOM 树。

那种数据覆盖式更新就是全量更新,全部都需要重新渲染

活学活用,大量数据的 diff 对比可以配合上方的 Web Workers 来进一步优化哦!

# 特性

  1. 数据比较:当数据更新时,系统会比较新旧数据,识别出具体哪些数据发生了变化。这个比较过程通常是基于某种形式的虚拟 DOM(如 React 中的虚拟 DOM)或其它数据对比机制实现的。
  2. 最小化 DOM 操作:根据比较结果,只对那些实际发生变化的数据对应的 DOM 元素进行更新。这种精确的更新避免了全面重绘,减少了浏览器的工作量,提升了渲染效率。
  3. 批量更新:在一些实现中,系统可能会收集一段时间内的所有数据变更,然后一次性计算差异并更新 DOM,这样可以进一步减少 DOM 操作的次数。
  4. 虚拟 DOM:在一些现代前端框架(如 React、Vue)中,差异更新是通过虚拟 DOM 来实现的。这种技术涉及在内存中维护一个 DOM 树的副本,当数据更新时,先在虚拟 DOM 上应用变更,然后计算新旧虚拟 DOM 之间的差异,并将这些差异应用到实际的 DOM 上。
  5. 用户体验:由于减少了不必要的 DOM 操作,差异更新可以大幅提高页面响应速度和流畅度,改善用户体验。
  6. 资源利用:差异更新策略更高效地利用了计算资源,尤其是在处理大型数据集和复杂界面时,能够显著减少浏览器的负担。

# 服务端渲染

# 介绍

服务端渲染(Server-Side Rendering,SSR)是一种在服务器上生成完整的页面 HTML 代码的技术,然后发送到客户端(浏览器),客户端加载这些 HTML 显示内容,而不需要等待所有 JavaScript 被下载和执行来呈现页面内容。

也就是后端将 HTML 代码渲染好给前端,我们的 VueReactSPA 程序,渲染全是在客户端,内容过多的话加载速度会拖慢卡顿,而且如果数据很大,客户端配置较差,那就更是难搞了。

所以我们直接在服务端就给页面渲染好,这样客户端压力就少了很多,渲染自然也是迅速了,SSR 本质是一种负担转移,将客户端压力转到了服务端。

而且 SSR 是 SEO 友好的,SPA 反之

VueReact 也有自己的 SSR 框架,分别是 NuxtNext ,尤其是 Next 非常好用。

# 原理

  1. 请求页面:当用户请求一个网页时,请求首先发送到服务器。
  2. 生成 HTML:服务器执行应用逻辑,访问数据库或调用 API 获取所需数据,然后将数据填充到模板中,生成完整的 HTML 页面。
  3. 发送响应:生成的 HTML 页面随后作为响应发送给客户端,客户端接收到 HTML 后,浏览器渲染显示给用户。
  4. 客户端接管:在客户端,一旦 JavaScript 加载并执行完成,网页通常会变成一个完全交互式的应用。这个过程称为 "Hydration",在这之后,页面交互将由客户端 JavaScript 接管。

# 优点:

  1. 提高性能:SSR 可以加快首次页面加载时间,因为浏览器获取到的是已经渲染好的 HTML,用户可以更快地看到页面内容。
  2. 优化 SEO:搜索引擎更容易抓取和索引服务端渲染的页面,因为它们可以直接分析已经渲染好的 HTML,而不需要执行 JavaScript。
  3. 更好的可访问性:由于内容直接在 HTML 中,即使在 JavaScript 被禁用或尚未执行时,用户也能看到基本的页面内容。

# 缺点:

  1. 服务器负载:每次页面请求都需要服务器动态生成 HTML,这可能会增加服务器的负载和响应时间。
  2. 开发复杂性:维护同一应用的客户端和服务器端渲染逻辑可能会增加开发和调试的复杂性。
  3. 限制:并非所有的 Web 应用都能从 SSR 中受益,特别是那些高度交互性的应用,客户端渲染可能是更合适的选择。

# 动画优化

其实动画优化包括了逐帧渲染,但是我还是分开来说比较好。

  1. 使用 CSS 动画而非 JavaScript 动画:
    • CSS 动画通常比 JavaScript 动画性能更好,因为浏览器可以对 CSS 动画进行优化,如在合适的时机使用硬件加速。
    • 使用 transitionanimation 属性来定义动画,而不是 JavaScript 的 setIntervalsetTimeout
  2. 利用硬件加速:
    • 对于某些属性,如 transformopacity ,浏览器可以利用 GPU 加速渲染,而不是仅依赖 CPU。这可以大大提高动画的性能。
    • 避免在动画中使用会引起回流(reflow)和重绘(repaint)的属性,如 widthheightmargintop 等。
  3. 使用 requestAnimationFrame (rAF):
    • 使用 requestAnimationFrame 来控制动画,而不是 setIntervalsetTimeoutrequestAnimationFrame 会在浏览器重绘之前执行动画代码,从而确保动画的流畅性。
  4. 简化动画元素:
    • 减少动画中涉及的元素数量和复杂性。更多的元素意味着更多的计算和渲染,这可能降低动画的性能。
    • 使用简单的形状和避免过度的细节。
  5. 优化动画执行时间:
    • 不要让动画运行超过必要的时间。长时间运行的动画不仅会消耗更多的 CPU 和 GPU 资源,还可能分散用户的注意力。
  6. 避免同时运行多个动画:
    • 同时运行的动画越多,对性能的影响就越大。如果可能,尝试减少同时运行的动画数量,或将多个动画合并为一个。
  7. 测试和分析:
    • 使用浏览器的开发者工具来分析动画的性能。注意查看动画是否引起了大量的重绘和回流,以及是否有性能瓶颈。
    • 在不同设备和浏览器上测试动画,确保它们在不同环境下都能流畅运行。

# 逐帧渲染

# 介绍

这个其实包含在动画优化内,不过我还是单独来介绍。

逐帧渲染(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)方法来实现。这里是其背后的一些关键原理:

  1. 时间同步:
    • requestAnimationFrame 调用一个函数来更新动画并在下一次浏览器重绘之前执行。这意味着您的动画帧与浏览器的刷新率(通常是 60 次 / 秒,即每 16.67 毫秒一帧)同步,从而最大化利用每一帧的渲染能力,确保动画平滑。
  2. 浏览器优化:
    • 使用 requestAnimationFrame 进行动画意味着浏览器能够优化动画的性能,减少或避免布局抖动(layout thrashing)和不必要的重绘(repaints),因为浏览器知道您的意图是创建动画,并可以为此做出优化。
    • 当标签页不在前台时,浏览器也会自动减少 requestAnimationFrame 的回调频率,以节省计算资源和电能。
  3. 帧状态更新:
    • 在每一帧中,您的代码应计算并更新动画的下一状态。这可以包括移动位置、改变颜色、调整大小等。因为您是在每一帧基础上进行更新,所以可以创建非常平滑和复杂的动画效果。
  4. 递归调用:
    • requestAnimationFrame 通常在被调用的函数内部再次调用自己,形成一个递归循环。这允许浏览器在下一个重绘之前再次执行动画更新逻辑,持续推进动画序列。
  5. 性能考量:
    • 由于 requestAnimationFrame 是与浏览器的刷新率同步的,它可以避免在屏幕刷新之间产生过多的帧,减少资源浪费,并提供流畅的视觉体验。

# 关于 16.67ms 如何得出的?

img

# 异步更新

简单来说,尽量的不要阻塞浏览器

对于异步就不多说了, JavaScript 是异步玩的非常出色的语言,小简就偷偷懒了。

# WebAssembly

# 介绍

WebAssembly(通常缩写为 Wasm)是一种为网络而生的新型代码格式,它允许在网页上运行与本地代码相近速度的程序。

WebAssembly 设计为与 JavaScript 协同工作,旨在成为 Web 开发者的另一种选择,特别是在性能敏感的应用程序中。

对于这个我了解的不多,没有实际使用过,但是我记得他可以将其他语言编译为 WebAssembly 格式在浏览器中执行,可以获得非常高的处理性能。

# 特点

  1. 高性能:WebAssembly 提供接近本地执行速度的性能,这是通过让 WebAssembly 代码在浏览器中经过更少的转换就可以直接执行来实现的。
  2. 安全:WebAssembly 维持了 Web 的安全特性,所有 WebAssembly 代码在一个沙盒环境中执行,确保了代码的运行不会对系统造成安全威胁。
  3. 可移植:WebAssembly 代码是以二进制格式分发,这使得它具有高度的可移植性,可以在不同的浏览器和平台上运行,而无需修改。
  4. 与 JavaScript 互操作:WebAssembly 设计为与 JavaScript 无缝协作,允许开发者在同一应用程序中同时使用 JavaScript 和 WebAssembly,利用各自的优势。
  5. 语言无关:虽然 WebAssembly 本身不是一种编程语言,但它提供了一个编译目标,使得多种语言(如 C、C++、Rust 等)可以编译为 WebAssembly 格式在浏览器中执行。

# 场景

  • 游戏开发:通过使用 WebAssembly,游戏开发者可以将现有的高性能游戏和图形引擎带到 Web 平台。
  • 音视频编解码:WebAssembly 可以用于加速视频的编解码过程,提供更流畅的播放体验。
  • 图形渲染:WebAssembly 的高性能特性非常适合需要大量计算的图形渲染任务。
  • 计算密集型应用:任何需要大量计算的应用,如数据分析或物理模拟,都可以从 WebAssembly 的使用中获益。

# 使用

虽然 WebAssembly 通常需要使用支持的编程语言编写后编译,但以下是一个简化的流程概述,没有具体代码但描述了从 C 到 WebAssembly 的一般步骤:

  1. 用 C 语言或者其他语言编写你的程序。
  2. 使用工具链(如 Emscripten)将代码编译为 WebAssembly(.wasm)文件。
  3. 在网页上通过 JavaScript 调用 WebAssembly 模块,与普通 JavaScript 对象和函数一同使用。

WebAssembly 现在正逐渐成为 Web 开发中的一个重要组成部分,提供了一种强大的方法来提升 Web 应用的性能和能力。

我目前没有使用过这个,单纯了解了一下,所以伪代码我也就不好写了,大家有兴趣可以去查阅其他资料。

# GPU 加速

# 介绍

GPU 加速是指利用图形处理单元(GPU)来加速图形渲染以及非图形计算的过程,以此提高应用程序的性能。

在 Web 开发领域,GPU 加速通常用于加速网页的图形和动画渲染,提供更流畅和响应更快的用户体验。

  1. 图形渲染:在传统的图形渲染过程中,大部分任务由中央处理单元(CPU)执行。GPU 加速使得这些任务可以转移到专为图形计算优化的 GPU 上,从而提高渲染速度和效率。
  2. CSS3 转换和动画:现代浏览器可以利用 GPU 加速 CSS3 的变换(transforms)和动画,这包括缩放(scale)、旋转(rotate)、位移(translate)等操作。
  3. Canvas 和 WebGL:Web 技术如 Canvas 2D 和 WebGL 也可以利用 GPU 加速来提升图形的绘制性能,这对于游戏和图形密集型应用尤为重要。
  4. 合成层:当网页的某些部分被浏览器识别为可以独立于其他部分变化时,它们可以被提升为合成层(composite layers),并由 GPU 独立渲染,从而提高整体渲染效率。

# 使用

可以通过某些 CSS 属性来提示浏览器使用 GPU 加速特定元素的渲染:

  1. 使用硬件加速的 CSS 属性:将 transform: translateZ(0)transform: translate3d(0, 0, 0) 应用于元素,可以创建一个新的合成层,即使这个变换本身没有视觉上的变化。
  2. 使用 will-change 属性:CSS 的 will-change 属性可以用来告知浏览器某个元素预计会有变化(如动画),浏览器可以提前进行优化。

# 注意

  1. 资源消耗:虽然 GPU 加速可以提高性能,但过度使用或不当使用也可能消耗更多资源,甚至降低性能。例如,创建过多的合成层可能会增加内存的消耗。
  2. 兼容性:不同设备和浏览器对 GPU 加速的支持程度可能不同,因此需要测试确保兼容性。
  3. 调试:开发者工具如 Chrome 的 Layers 面板可以用来查看页面的合成层,帮助开发者理解和优化 GPU 加速的使用。

# 尾述

开年第一篇结束,先就说到这吧,打字手都打麻了,如果可以的话,麻烦给我点个赞吧,如果需要转载记得署名小简然后备注来源哦!原创不易,感谢支持。

对了,推荐两篇关于瀑布流的大量数据渲染优化方案:

  1. 瀑布流使用虚拟列表性能优化:juejin.cn/post/716607…
  2. 关于双列瀑布流布局的优化思考:juejin.cn/post/692163…
  3. 瀑布流优化:juejin.cn/post/732797…