燧石受到的敲打越厉害,发出的光就越灿烂。——卢梭

大家好,我是柒八九

前言

打工人 打工魂 打工人都是人上人。是不是还沉浸在2024的放假通知中,小伙该收收心了。毕竟,你多打一天的工,老板就离他在游艇中喝着香槟和美女一起海钓的梦想又更进一步了。好了,玩归玩,闹归闹。作为一个职业打工人,我们还是要着眼于当下。

在前几天,我们写了一篇Rust 编译为WebAssembly 在前端项目中使用的文章,简单的描述了Rust如何编译为wasm在浏览器中使用,本意是想表达Rustwasm是可以在浏览器中使用,并且还有更深的意思就是wasm在前端真的真的会有大放异彩的一天。在发布文章后,在一些平台中,总有人充斥着质疑声。

大概,他也是出于一些好意,然后也想找一些理由,让我们迷途知返,幡然醒悟。我认为想要说服一个人,讲事实,摆道理是一个最优路线。当然,我也没想着通过几句话说服别人。那就说的委婉点哇,那就用事实和道理,说服我自己,让我能够更有动力去学习。

莫言曾说做人切记:法不轻传,道不贱卖,师不顺路,医不叩门,你永远叫不醒一个装睡的人,即便你再唤醒他,他是否愿意醒还是个问题。绝大部分人活着都是为了睡得更香,而不是为了觉醒。 虽然这话在这里有点重,但是我认为也可以作为一个做事准则,不要好为人师。

在前面的文章中多次提到,国内技术存在滞后性,而大部分抗拒Rust/Wasm的人,也是拿国内的环境说事。其实吧,我不是崇洋媚外之人,但是不得不承认有些东西,国外的月亮确实比较圆。(如果这句刺痛了你,不好意思,这是我的无心之举。我是一个坚定的马克思主义理论工作者)

今天,我们就以国外一篇文章Photoshop is now on the web!为主体框架,来讲讲Photoshop团队通过WebAssembly + EmscriptenWeb Components + LitService Workers + Workbox以及新的Web API,如何将一个桌面重应用,迁移到浏览器环境下的。其代表着将高度复杂和图形密集型软件引入浏览器的一个巨大里程碑

在将如此重的应用搬上浏览器是一件极其伟大的事情,这其中涉及了很多新奇的技术还有性能优化的东西,并且通过学习它的实现过程,我们还可以从中散发到我们平时的开发任务中。

这就是,站在巨人的肩膀上,你会看的更高

文中出现了很多我们之前介绍过的东西。我们会按照我本人的知识体系做一定的删减和增加。放心,内核的东西都不会丢。如果大家想观看原文,可以查看原文。(原文只是一些知识体系的罗列,相信大家两者都看了,会有一个清晰的判断)

好了,天不早了,干点正事哇。

我们能所学到的知识点

  1. 前置知识点
  2. 愿景:将Photoshop引入浏览器
  3. 新的Web功能释放了Photoshop的潜力
  4. 优化Photoshop在浏览器中的性能
  5. 使用TensorFlow.js集成本地设备上的机器学习

1. 前置知识点

前置知识点,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略

同时,由于阅读我文章的群体有很多,所以有些知识点可能我视之若珍宝,尔视只如草芥,弃之如敝履。以下知识点,请酌情使用

源(origin)


源(origin)是

  1. 协议,例如 HTTPHTTPS
  2. 主机名
  3. 端口(如果有的话,HTTP的默认端口是80,而HTTPS的默认端口是443

的组合。

例如,给定网址 https://www.example.com:443/foo,它的originhttps://www.example.com:443

同源(same-origin)和跨源(cross-origin)

具有相同协议、主机名和端口组合的网站会被视为同源网站。所有其他项都被视为**跨源
Origin AOrigin B是否“同源”或“跨源”
<nobr>https://www.A.com:443 </nobr><nobr>https://www.B.com:443</nobr>跨源:不同的域名
<nobr> https://login.A.com:443 </nobr>跨源:不同的子域名
<nobr> http://www.A.com:443 </nobr>跨源:不同的协议
<nobr> https://www.A.com:80 </nobr>跨源:不同的端口
<nobr> https://www.A.com:443 </nobr>同源:完全匹配
<nobr> https://www.A.com </nobr>同源:隐式端口号匹配(443)

Blob 数据类型

Blob(Binary Large Object)是一种二进制大型对象数据类型,它代表了一段任意类型的二进制数据。Blob 数据通常用于存储大量的二进制数据,如图像、音频、视频、文件等。

  1. 创建 Blob 对象:

    可以使用构造函数 BlobBlob() 工厂函数来创建 Blob 对象。Blob 构造函数接受一个数组(通常是 Uint8Array 数组)作为参数,这些数组将被组合成一个 Blob 对象。

    const textData = 'Hello, Blob!';
    const blob = new Blob([textData], { type: 'text/plain' });

    上述代码创建了一个包含文本数据的 Blob 对象,并指定了数据类型为纯文本。

  2. Blob 类型:

    Blob 对象可以包含不同类型的数据,例如文本、图像、音频、视频等。通过设置 type 参数,可以指定 Blob 对象的数据类型。以下是一些常见的 Blob 类型:

    • 'text/plain': 纯文本数据。
    • 'image/jpeg': JPEG 图像数据。
    • 'audio/mp3': MP3 音频数据。
    • 'video/mp4': MP4 视频数据。
    • 'application/pdf': PDF 文件数据。
  3. Blob 方法:

    Blob 对象具有一些方法,使我们可以执行以下操作:

    • slice(start?: number, end?: number, contentType?: string): 创建并返回 Blob 对象的切片。
    • stream(): 返回一个 ReadableStream,可用于逐块读取 Blob 数据。
    • text(): 返回 Blob 数据的文本表示。
    • arrayBuffer(): 返回 Blob 数据的 ArrayBuffer
    • size: Blob 数据的大小,以字节为单位。
    • type: Blob 数据的 MIME 类型。
  4. Blob 用途:

    Blob 对象在前端开发中广泛用于以下方面:

    • 加载和展示图像、音频和视频。
    • 上传文件和数据到服务器。
    • 缓存资源以提高性能,如 Service Workers
    • 读取本地文件以进行处理或预览。

用途

FileReaderURL.createObjectURL()createImageBitmap()XMLHttpRequest.send() 可以接受Blob对象用于特定的数据处理。

  1. FileReader:

    FileReader 是用于读取文件内容的 JavaScript 对象。要将 Blob 数据展示,可以使用 FileReader 读取 Blob 数据,然后在读取完成后执行回调函数来处理数据。

      // 选择文件的输入元素
     const fileInput = document.getElementById('fileInput'); 
      // 用于显示图像的 <img> 元素
     const imageElement = document.getElementById('imageElement');
    
     fileInput.addEventListener('change', function (e) {
         const file = e.target.files[0];
         if (file) {
             const reader = new FileReader();
             reader.onload = function (e) {
                 // 将 <img> 的来源设置为 Blob 数据
                 imageElement.src = e.target.result; 
             };
             // 以数据 URL 的形式读取 Blob 数据
             reader.readAsDataURL(file); 
         }
     });
  2. URL.createObjectURL():

    URL.createObjectURL() 是用于创建 Blob URL 的函数。我们可以将 Blob 数据转换为 Blob URL,然后将其分配给支持 Blob URL 的 HTML 元素,例如 <img><a>

     const blob = new Blob(['前端柒八九!'], { type: 'text/plain' });
     const blobURL = URL.createObjectURL(blob);
     // 一个用于链接到 Blob 的 <a> 元素
     const linkElement = document.getElementById('linkElement'); 
     // 将 Blob URL 分配给链接的 href 属性
     linkElement.href = blobURL; 
  3. createImageBitmap():

    createImageBitmap() 是用于创建图像位图的函数。我们可以使用它来处理 Blob 数据并将其转换为图像位图,然后将位图绘制到支持绘图的 HTML 元素上。

    // 一个 <canvas> 元素
    const canvas = document.getElementById('canvas'); 
    const blob = new Blob(['Your Blob Data'], { type: 'image/jpeg' });
    
    createImageBitmap(blob).then(function (imageBitmap) {
        const context = canvas.getContext('2d');
        context.drawImage(imageBitmap, 0, 0);
    });
  4. XMLHttpRequest.send():

    使用 XMLHttpRequest 可以将 Blob 数据发送到服务器,或者从服务器获取 Blob 数据并展示它。以下是一个获取并展示图片的示例:

    const xhr = new XMLHttpRequest();
    xhr.open('GET', 'your-image-url.jpg', true);
    xhr.responseType = 'blob';
    xhr.onload = function () {
        if (this.status === 200) {
            const blob = this.response;
            const blobURL = URL.createObjectURL(blob);
            // 用于显示图像的 <img> 元素
            const imageElement = document.getElementById('imageElement'); 
            imageElement.src = blobURL;
        }
    };
    xhr.send();

2. 愿景:将Photoshop引入浏览器

几十年来,Photoshop一直是图像编辑和图形设计的王者,兼容WindowsmacOS两个皆然不同的系统。但将其从桌面解放出来,就像打开了新世界的大门,让我们对未来的浏览器应用有了更多的展望和遐想。

  • Web便捷性为用户可以仅通过浏览器即可开始编辑和协作,无需安装。而且他们可以在不同设备之间无缝切换
  • 可链接性使工作流程共享成为可能。Photoshop文档可以通过URL访问,而不是把我们的心神淹没在文件系统中。创作者可以轻松地将链接发送给合作者。
  • 跨平台的灵活性。Web作为高级载体,可以过滤掉底层操作系统。Photoshop可以触达多个平台的用户。

然而,实现这一愿景面临着重大的技术挑战,需要重新思考像Photoshop这样强度大的应用程序如何在Web上运行。

3. 新的Web功能释放了Photoshop的潜力

近年来,通过标准化和实现,新的Web功能如雨后春笋般的涌现,最终可以实现类似Photoshop的应用程序。

3.1 使用Origin Private File System实现高性能本地文件访问

Photoshop的操作涉及读写可能非常庞大的PSD文件。这需要对本地文件系统进行有效的访问。新的Origin Private File System API(OPFS)提供了一个快速的、特定于来源的虚拟文件系统

兼容性

看到一个新的技术,我们的第一反应就是它的兼容性如何。毕竟,想在浏览器中大放异彩,需要宿主的支持。下图是OPFS的在桌面浏览器中的支持程度-92%是一个不错的结果。那就意味着,我们可以放心大胆的在主流的浏览器中使用它了。这是一个很好的开局。

概念介绍

私有文件系统(OPFS)是文件系统API的一部分,是页面的来源提供的存储端点,不像常规文件系统那样对用户可见。它提供对一种特殊类型的文件的访问,经过高度优化以提供性能,并提供内容的就地写入访问。

上面提到OPFS与常规文件系统是不一样的。OPFS并不能被用户看到。顾名思义,OPFS中的文件和文件夹不是面向用户的。OPFS中的文件和文件夹是基于网站的origin私有的。例如:网页https://A.com/B/的源是https://A.com/(:443),所有共享相同origin的页面可以查看相同originOPFS数据,因此https://A.com/C/test 可以查看与https://A.com/OPFS数据。

每个origin都有自己独立的OPFS,这意味着https://A.comOPFShttps://B.com 等站点的OPFS完全不同。

而对于OPFS的存储形式,我们可以参照本地系统。在Windows上,用户可见文件系统的根目录是 C:\。对于OPFS,相当于每个origin都可以通过调用异步方法 navigator.storage.getDirectory()访问一个最初为空的OPFS根目录。

就像浏览器中的其他存储机制(例如 localStorageIndexedDB)一样,OPFS也受浏览器配额限制。如果用户清除所有浏览数据或所有网站数据,OPFS也会被删除。

使用方式

使用OPFS的方法有两种:在主线程上或在 Web Worker 中使用。

  • Web Worker 不能阻塞主线程,这意味着在此上下文中,API 可以同步,同步 API 的速度更快,因为它们无需处理 promise
  • 主线程上通常不允许同步API

无论是在主线程上或在 Web Worker 中使用,第一步首先就是获取对根目录的访问权限,这样OPFS使得可以快速创建、读取、写入和删除文件。

const opfsRoot = await navigator.storage.getDirectory();

有了根文件夹后,我们分别使用 getFileHandle()getDirectoryHandle() 方法创建文件文件夹。传递 {create: true} 后,系统会创建不存在的文件或文件夹。以新创建的目录为起点调用这些函数,以构建文件层次结构。

const fileHandle = await opfsRoot
    .getFileHandle('my first file', {create: true});
const directoryHandle = await opfsRoot
    .getDirectoryHandle('my first folder', {create: true});
const nestedFileHandle = await directoryHandle
    .getFileHandle('my first nested file', {create: true});
const nestedDirectoryHandle = await directoryHandle
    .getDirectoryHandle('my first nested folder', {create: true});

最终形成的目录结构如下:

getFileHandle() getDirectoryHandle() 方法不仅可以创建新的文件或者文件夹,我们还可以通过指定特定的参数,来访问先前创建的文件和文件夹。

const existingFileHandle = await opfsRoot.getFileHandle('my first file');
const existingDirectoryHandle = await opfsRoot
    .getDirectoryHandle('my first folder');

既然,文件目录有了,我们更希望的是能够在其中存储相关的数据信息。此时我们通过调用 createWritable() 将数据流传输到文件中,这会创建一个指向该文件的 FileSystemWritableFileStream,然后通过 write() 写入相应内容。最后,对数据流执行 close() 操作。

const contents = '前端柒八九';
// 获取可写流。
const writable = await fileHandle.createWritable();
// 将文件内容写入流。
await writable.write(contents);
// 关闭流,从而保存文件内容。
await writable.close();

前面,讲过OPFS并不能被用户看到,在前面的操作中,我们新建的文件,写入了内容,此时所有的操作都是对用户不可见的,那如果没有方式让这些数据可见,那岂不是脱裤子放屁,多此一举。好在,人家已经给我们想好招了。

我们可以通过fileHandle.getFile()获取关联的 File对象。File 对象是一种特定类型的 Blob,可以在 Blob 能够使用的任何上下文中使用。这样我们就可以通过指定的API(在前置知识点中有过介绍)将其转换成其他数据类型。并且我们可以访问这些转换后的数据,并将其提供给用户可见的文件系统

const file = await fileHandle.getFile();
console.log(await file.text());

上面是OPFS的基础语法,其实要想发挥其最大的功效,还是需要借助Web Worker。毕竟,我们既然用到了OPFS,那肯定是要解决在浏览器中操作大文件所遇到的阻塞主线程等令人抓狂的性能问题。

并且,由于Web Worker 不会阻塞主线程,因此在此上下文中允许使用OPFS的同步方法。

我们可以通过同步句柄,来操作对应的文件。同步句柄可以通过调用 createSyncAccessHandle() 从常规 FileSystemFileHandle 中获取。

const fileHandle = await opfsRoot
    .getFileHandle('my highspeed file.txt', {create: true});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();

有了同步访问句柄后,我们就可以以极其快速且同步的方式操作文件。

  • getSize():返回文件的大小(以字节为单位)。
  • write():将缓冲区的内容写入文件(可选在给定偏移量处),并返回写入的字节数。检查返回的写入字节数,允许调用方检测并处理错误及部分写入。
  • read():将文件内容读取到缓冲区(可以选择在给定偏移量处)。
  • truncate():将文件大小调整为指定大小。
  • flush():确保文件内容包含通过 write() 完成的所有修改。
  • close():关闭访问句柄。

这个本地高性能文件系统对于在浏览器中实现PS的高要求文件工作流程至关重要。

启发

想必大家或多多少的知晓,在传统桌面版本的PS,要处理一个文件是很大的。但是,PS团队确利用了OPFS完美的解决了这个顽疾。其实,这也算是给我们一个莫大的启发,如果我们以后在接到类似要操作大文件的需求时候,在即有技术不满足性能要求的情况下,是不是可以利用OPFS来为我们开辟一个新思路。

案例提供

假如,现在我们有一个体积很大的 <canvas> 元素,我们想在页面中进行展示,但是这个文件不变的,如果我们每次通过网络加载,并且每次都渲染的话,那在每次页面状态变更的时候,会有一小段页面真空时段,这是我们无法忍受的。 那么我们是不是换种方式,将该<canvas>转换为Blob -PNG的形式,并且存储到OPFS中,在合适的方式进行数据的展示。

async function doOpfsDemo() {
    // 打开网站(origin)的私有文件系统的“根目录”:
    let storageRoot = null;
    try {
        storageRoot = await navigator.storage.getDirectory();
    } catch (err) {
        console.error(err);
        alert("无法打开 OPFS。请查看浏览器控制台。\n\n" + err);
        return;
    }

    // 从页面 DOM 获取 <canvas> 元素:
    const canvasElem = document.getElementById('myCanvas');

    // 保存图像:
    await saveCanvasToPngInOriginPrivateFileSystem(storageRoot, canvasElem);

    // 重新加载图像:
    await loadPngFromOriginPrivateFileSystemIntoCanvas(storageRoot, canvasElem);
}

async function saveCanvasToPngInOriginPrivateFileSystem(storageRoot, canvasElem) {
    // 将 <canvas> 的图像保存为 PNG 文件到内存中的 Blob 对象:(参考:https://stackoverflow.com/a/57942679/159145)
    const imagePngBlob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));

    // 在新的子目录 "art" 中创建一个空(零字节)文件:"mywaifu.png":
    const newSubDir = await storageRoot.getDirectoryHandle("art", { "create": true });
    const newFile = await newSubDir.getFileHandle("mywaifu.png", { "create": true });

    // 以可写流的形式(FileSystemWritableFileStream)打开 `mywaifu.png` 文件:
    const wtr = await newFile.createWritable();
    try {
        // 直接写入 Blob 对象:
        await wtr.write(imagePngBlob);
    } finally {
        // 安全地关闭文件流写入器:
        await wtr.close();
    }
}

async function loadPngFromOriginPrivateFileSystemIntoCanvas(storageRoot, canvasElem) {
    const artSubDir = await storageRoot.getDirectoryHandle("art");
    const savedFile = await artSubDir.getFileHandle("mywaifu.png");

    // 将 `savedFile` 作为 DOM `File` 对象获取(与 `FileSystemFileHandle` 对象不同):
    const pngFile = await savedFile.getFile();

    // 将其加载到 ImageBitmap 对象中,可以直接绘制到 <canvas>。不再需要使用 URL.createObjectURL 和 <img/>。参考:https://developer.mozilla.org/en-US/docs/Web/API/createImageBitmap
    // 但仍然需要在绘制后 `.close()` ImageBitmap,否则会出现内存泄漏。使用 try/finally 块处理这个问题。
    try {
        const loadedBitmap = await createImageBitmap(pngFile);
        try {
            const ctx = canvasElem.getContext('2d');
            ctx.clearRect(/*x:*/ 0, /*y:*/ 0, ctx.canvas.width, ctx.canvas.height); // 在绘制加载的图像之前清除画布。
            ctx.drawImage(loadedBitmap, /*x:*/ 0, /*y:*/ 0);
        } finally {
            loadedBitmap.close();
        }
    } catch (err) {
        console.error(err);
        alert("无法将以前保存的图像加载到 <canvas> 中。请查看浏览器控制台。\n\n" + err);
        return;
    }
}
世上本没有路走的人多了也就成了路

如果想了解更多OPFS可以参考


3.2 发挥WebAssembly的威力

WebAssembly对于在JavaScript中重新创建Photoshop的计算密集型图形处理是一个不可或缺的要素。Adobe使用Emscripten编译器将他们现有的C/C++代码库移植到WebAssembly模块中。

兼容性

还是熟悉的配方,我们通过caniuse来查看,桌面浏览器对WebAssembly的支持程度。哇塞,形势一片大好。各大厂商都意识到这个神兽能给我们带来更多意想不到的可能性。

其实,暂且不看市面上公司如何使用,从各个厂商的积极程度也侧面反应了,我们的预期。

几个WebAssembly的功能至关重要:

  • 线程 - Photoshop使用工作线程以并行方式执行任务,比如处理图像块
  • SIMD - SIMD矢量指令加速像素操作和过滤。
  • 异常处理 - C++异常广泛用于整个Photoshop的代码库。
  • 流式实例化 - Photoshop的80MB+ WASM模块需要流式编译。
  • 调试 - Chrome的WebAssembly在DevTools中的调试支持是非常有价值的。

如果大家对WebAssembly还不是很了解的话,可以翻阅我们之前写的浏览器第四种语言-WebAssembly。针对wasm的概念性东西这里就不在过多介绍了。在里面我们还介绍了利用Emscripten实现了 将C/C++代码编译为WebAssembly

并且,在我们之前还介绍过,CJS代码之间的互操作。感兴趣的可以参看WebAssembly-C与JS互相操作

而我们来讲讲SIMD的东西。


SIMD

SIMD代表单指令,多数据。是Single Instruction, Multiple Data的缩写。
SIMD操作这个术语指的是一种计算方法,它能够通过单个指令来处理多个数据。相比之下,传统的顺序方法使用一条指令来处理每个单独的数据,这被称为标量操作

以简单的加法为例,下面说明了标量操作SIMD操作之间的差异。


使用传统的标量操作,必须依次执行四个加法指令才能获得如图(a)所示的总和。与此同时,SIMD只使用一条加法指令就能获得相同的结果,如图(b)所示。由于处理相同数量的数据所需的指令更少,SIMD操作比标量操作具有更高的效率

这里简单的说一句题外话:标量这个词是不是感觉似曾相识。其实,我们在介绍Rust数据结构的时候就有过接触呢。


这下估计就知道标量操作就是单一操作了。

SIMD指令是一类特殊指令,通过同时对多个数据元素执行相同的操作,来充分利用应用程序中的数据并行性。计算密集型应用程序,如音频/视频编解码器、图像处理器,都是利用SIMD指令来加速性能的示例。

SIMD的限制

尽管SIMD操作具有能够在一条指令中处理多个数据的优势,但它们只能应用于特定预定义的处理模式。下图展示了一个这样的模式,在该模式中,所有数据都执行相同的加法操作

SIMD操作不能用于以不同方式处理多个数据。下图中提供了一个典型的示例,其中一些数据需要相加,而其他数据需要相减、相乘或相除。

SIMD操作就是需要所有数据都是执行相同的操作)

想了解更多关于SIMD概念性的东西,可以参看SIMD基础介绍 (需要🪜)

由于SIMDwasm的特性,而我们的关注点又是浏览器环境,我们就挑一个我们熟悉的浏览器chrome来描述一下,所以我们看看V8是如何支持这个特性的。(V8chrome/chromium的关系我们之前聊过)

由于可编译为wasm的语言过于多,例如C/C++/Rust甚至TypeScript。所以,我们就挑一个我们比较感兴趣的语言来说明。

将Rust代码编译为WebAssembly SIMD

当将Rust代码编译为WebAssembly SIMD目标时,我们需要启用与Emscripten中的simd128 LLVM特性相同的特性。

当启用msimd128特性时,默认情况下启用LLVM的自动矢量化器,即在优化级别-O2-O3时启用。

如果可以直接控制rustc标志或通过环境变量RUSTFLAGS,可以传递-C target-feature=+simd128

rustc … -C target-feature=+simd128 -o out.wasm

或者

RUSTFLAGS="-C target-feature=+simd128" cargo build

例如,考虑以下函数,该函数将两个输入数组的元素相乘并将结果存储在输出数组中。

pub fn multiply_arrays(out: &mut [i32], in_a: &[i32], in_b: &[i32]) {
  in_a.iter()
    .zip(in_b)
    .zip(out)
    .for_each(|((a, b), dst)| {
        *dst = a * b;
    });
}

如果没有传递msimd128特性,编译器会生成以下WebAssembly循环:

(loop
  (i32.store
    … 获取`out`中的地址 …
    (i32.mul
      (i32.load … 获取`in_a`中的地址 …)
      (i32.load … 获取`in_b`中的地址 …)
  …
)

但是,当使用msimd128特性时,自动矢量化器会将其转换为包含以下循环的代码:

(loop
  (v128.store align=4
    … 获取`out`中的地址 …
    (i32x4.mul
       (v128.load align=4 … 获取`in_a`中的地址 …)
       (v128.load align=4 … 获取`in_b`中的地址 …)
    …
  )
)

循环体的结构相同,但在循环体内部使用SIMD指令加载、相乘和存储四个元素


启发&应用场景

与其说是启发,倒不如说是,WebAssembly能够给我们带来多大的惊喜。在之前的文章中,我们聊过WebAssembly在哪些场景下能够大放异彩。 而今天既然聊到了WebAssembly -SIMD,那我们到底看看它是否还有绝活

WebAssembly SIMD提案旨在加速高计算应用程序,如音频/视频编解码器、图像处理应用程序、加密应用程序等。

MediaPipe是一个用于构建多模式(例如视频、音频、任何时间序列数据)应用机器学习管道的框架。

其中一个视觉效果最吸引人的演示,可以很容易地观察到SIMD带来的性能差异,是一个仅使用CPU构建的手部跟踪系统。没有启用SIMD时,在现代笔记本电脑上只能获得大约14-15帧每秒(FPS),而在Chrome Canary中启用SIMD后,您可以获得更平滑的体验,帧率可达38-40帧每秒。

OpenCV,这是一个流行的计算机视觉库,也可以编译成WebAssembly。

OpenCVIntel开源计算机视觉库。它由一系列C 函数和少量C++ 类构成,实现了图像处理和计算机视觉方面的很多通用算法。

其中它有一个WebCamera的项目,如下图。做了一下用于图像和视觉处理的工具。

此图,就是用于用摄像头识别银行卡中的帐号数据,并对齐提取。

那是不是,在我们遇到类似的功能点时,我们在已经引入到对应的视觉处理工具后,效果还没达到我们的心里预期,那WebAssembly SIMD不就有了用武之地了吗。

不仅,视觉处理,还有音频处理也是相同的道理。这里就不展开说明了。


3.3 将sRGB替换为P3

还有老样子,让我们can一下。

DCI-P3,有时也称为P3Display P3DCIDigital Cinema Initiatives的缩写,是数字电影领域的一个标准。

sRGB代表标准红绿蓝,是一种颜色空间,也是一组特定的颜色,由惠普和微软于1996年创建,旨在标准化电子设备中显示的颜色。sRGB是目前最流行的颜色空间,用于Windows、大多数网络浏览器以及大多数控制台和个人电脑游戏,除非它们支持高动态(HDR)。

显示器或其他设备的色域告诉我们设备可以重现哪个颜色空间,以及它可以在0-100%甚至更高的范围内呈现多少颜色。除了sRGB,其他常见的颜色空间还包括Adobe RGBP3,它们都比sRGB更大,即包含更多颜色

下面是XY色度图,代表了人眼可以看到的所有颜色范围。在那个颜色范围内,有一个白色的三角形,它勾勒出sRGB标准所包括的颜色。例如,如果我们试图在sRGB显示器上查看超出该三角形范围的颜色的图像,那些额外的颜色会显得不准确和饱和不足

sRGB vs P3

虽然sRGB是标准,但其他颜色空间也可能更具吸引力。P3表现色域比sRGB更大(覆盖更多颜色)。

P3显示的色域比sRGB显示器宽50%


白线显示了sRGB的边缘。它上面右边的部分是Display-P3颜色,而这些颜色在sRGB中是不可用的。请注意,绿色扩展得很大,而蓝色扩展则远远不及。

sRGBP3之间的另一个区别是P3可以处理10位颜色

Photoshop使用新的color()函数和Canvas API来释放P3的全部光彩,实现更准确的颜色呈现。

color: color(display-p3 1 0.5 0)

上面的语法等同于旧有的css表示颜色的语法。

  • hsl(42, 70%, 50%)
  • rgb(3, 5, 11)
  • #abc

如果我们想使用p3可以使用@supports功能查询。

/* sRGB颜色。 */
:root {
    --bright-green: rgb(0, 255, 0);
}

/* Display-P3颜色,如果支持的话。 */
@supports (color: color(display-p3 1 1 1)) {
    :root {
        --bright-green: color(display-p3 0 1 0);
    }
}

header {
    color: var(--bright-green);
}

启发

如果我们在项目中,想要实现一下对色彩饱和度有强烈要求的功能,那么我们就可以使用color(display-p3)来增强我们的项目。虽然,写不出五彩斑斓的黑,红的发紫的白。但是这也算是我们扩展应用功能的一把利器。

好钢要用在刀刃上


3.4 利用Web组件提升UI的灵活性

Photoshop利用基于Lit构建的标准化Web组件策略,可以实现应用程序之间的UI一致性。


Photoshop的用户界面元素来自AdobeSpectrum Web Components库,该库实现了Adobe的设计系统。

Spectrum Web Components具备以下特点:

  • 默认支持辅助功能 — 开发时考虑了现有和新兴的浏览器规范,以支持辅助技术。
  • 轻量级 — 使用LitElement实现,减小了额外开销。
  • 基于标准 — 基于Web Componment标准,如自定义元素和Shadow DOM 构建。
  • 框架无关 — 由于浏览器级别的支持,可以与任何框架一起使用。

此外,整个Photoshop应用程序都是使用基于LitWeb组件构建的。Lit的模板和虚拟DOM diff使得UI更新更加高效。Web组件的封装性也使得在需要时可以轻松集成来自其他团队的React代码。

我们在之前的文章中也有对Lit有过涉猎,本人也在项目中也有对应的简单应用。咋说呢,感觉Lit的封装还是很nice的。但是,如果硬要刨根问底的话,其实还是要从Web Componment来讲。

如果想了解Lit可以通过Lit 官网去学习和查阅。这里就不在多讲。

虽然,现在Vue/React等,框架大行其道,但是Web Componment中的一些理念和使用方式是最贴合浏览器渲染机制的。然后,由于篇幅有限,我们打算,针对Web Componment会做一次深度的解析和教学。

启发

虽然,不知道PS团队,不知出于何种目的,选用了Lit作为了构建前端页面的UI库。但是,想象一下,如果现在我们有一个大前端团队,由于历史包袱原因,即有React,又有Vue团队。现在有一个庞大的功能需求需要全团队配合去做。那是不是利用Lit或者Web Componment可以解决这个痛点。

但是呢,使用Lit只是一个跨语言合作的一个可行方案,我相信市面上肯定有很多解决方案,例如:Veaury

还有很多巨石应用的解决方案。这里也不过多展开了。


4. 优化Photoshop在浏览器中的性能

尽管新的Web功能提供了基础,但像Photoshop这样的高强度桌面应用程序仍然需要进行大量的跟踪和性能优化工作,以提供一流的在线体验。

4.1 使用Service Workers缓存资源和代码

Service Workers允许Web应用将其资源、代码和其他资源本地缓存,以在初始访问后加载速度更快。虽然它还不是一个完全离线可用的应用程序,但Photoshop已经利用Service Workers来缓存其WebAssembly模块、脚本和其他资源。

可以看到一些JavaScript块已经被分割并本地缓存,从而实现了非常快速的后续加载。

Adobe使用Workbox库更轻松地将Service Worker缓存集成到他们的构建过程中。

针对Worker呢,我们之前的文章就有过介绍。

分别对Web WorkerService Worker做了一次比较透彻的分析。

然后呢,其实在Service Worker始终有一种欲罢不能的感觉。就像上面说的,Photoshop是借助了Workbox实现了资源的本地缓存。同时,我也是第一次听说这个技术(Workbox),然后就这两天开始着手找资料学习和研究。发现,其中的很多点都很喜欢,然后也是有很多的点可讲。但是呢,由于现在有些东西还未亲身实践,所以有些想着,等我实践完然后准备写一篇或者多篇的文章,深度解析一下这个东西。

在这里,我就偷个懒了哈,不过更文列表中已经有计划了。优先级还很高呢。


4.2 V8对已缓存资源的优化

当资源从Service Worker缓存中返回时,V8会进行一些优化:

  • 在安装期间缓存的资源会被急切地编译,并立即进行代码缓存,以保持一致、快速的性能。
  • 通过Cache API缓存的资源在第二次加载时进行了优化缓存,比通常的缓存速度更快。
  • V8会检测已缓存资源的重要性,并更积极地进行编译。

这些优化允许对Photoshop庞大的缓存Wasm模块进行优化。


4.3 流式传输和缓存大型WebAssembly模块

Photoshop的代码库需要多个大型WebAssembly模块,其中一些超过80MBV8Chrome中的流式编译支持使这些大型模块可以在性能方面进行有效处理。

也就是说,V8不用讲wasm模块全盘接收,再开启编译模式。这样在很大程度上节省了时间,尤其在遇到大文件的时候。

此外,第一次从Service Worker请求WebAssembly模块时,V8会生成并存储一个优化版本以进行缓存,这对于Photoshop庞大的代码尺寸至关重要。

4.4 用于并行图形操作的多线程

Photoshop中的许多核心图像处理操作,如像素变换,可以通过跨线程并行执行来大幅加速。WebAssembly的线程支持可以利用多核设备进行计算密集型图形任务

这允许Photoshop在转移到WebAssembly后,使用与桌面相同的多线程方法来处理性能关键的图像处理函数。

4.5 用于优化的WebAssembly调试

在开发过程中,强大的WebAssembly调试支持对于诊断和解决性能瓶颈至关重要。

Chrome DevTool的能力可以对WASM代码进行性能分析、设置断点,并检查丰富的变量,这与JavaScript的调试性质相似。


5. 使用TensorFlow.js集成本地设备上的机器学习

最近版本的Web上的Photoshop包括使用TensorFlow.js的AI功能。在设备上运行模型而不是在云端改善了隐私、延迟和成本。

TensorFlow.jsGoogle推出的针对JavaScript开发者的开源机器学习库,能够在浏览器中客户端运行。它是用于Web机器学习的最成熟选择,具有全面的WebGLWebAssembly后端操作支持,未来还将提供WebGPU后端选项,以在浏览器中获得更快的性能,以适应新的Web标准的发展。"

"选择主题"功能利用机器学习自动提取图像中的主要前景对象,大大加速了复杂的选择操作。

为了实现本地执行,该模型从TensorFlow转换为TensorFlow.js

// 加载“选择主题”模型
const model = await tf.loadGraphModel('select_subject.json');

// 在图像张量上运行推理
const {mask, background} = model.execute(imgTensor);

Adobe和Google合作开发了一个用于Emscripten的代理API,以解决Photoshop的WebAssembly代码和TensorFlow.js之间的同步问题,从而实现了这两个框架的无缝集成。

“由于Google团队通过其各种支持的后端(WebGL、WASM、Web GPU)提高了TensorFlow.js的硬件执行性能,这导致模型的性能改进在30%到200%之间(尤其是对于倾向于获得最大性能提升的较大模型而言),在浏览器中几乎实时性能。”

启发

这个点,算是继wasm在浏览器中实现,又一个让我眼前一亮的特性。现在大家都在卷各种大模型,国内国外都是如此,我们公司也有自己的AI团队。而大部分的开发模式,基本上都是将AI模型配置到后端,然后前端页面都是通过异步接口进行传值处理。其实这和旧有的前端开发模式没有任何的改变。

但是,在PS团队实现了基于TensorFlow.js的前端AI模型,那是不是变现的说,万物即可AI,并且在前端也会有一席之地。

等着,给我一段时间,到时候给大家出一篇在前端界面中使用TensorFlow.js的教程。(这个是真心喜欢研究的东西)


后记

写到这里,其实里面的内容过于多,本来不想再继续聒噪了,但是还是有感而发。

人人都说前端已死,但是你如果看到上面的一些技术和特性,你还会有这种感觉吗。

不要学着别人去,自怨自艾,只有自己内心渴望一个东西,你才会有勇气和动力去面对和征服它。

分享是一种态度

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。


前端柒八九
18 声望3 粉丝