前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >WebAssembly 和 JavaScript 该怎么选?

WebAssembly 和 JavaScript 该怎么选?

作者头像
ConardLi
发布2024-01-23 16:54:16
1520
发布2024-01-23 16:54:16
举报
文章被收录于专栏:code秘密花园code秘密花园

JavaScript 已经长久以来并且目前依然是浏览器运行时的主流开发语言,然而近年来,WebAssembly 的诞生为我们提供了一个全新的选择。这就引出了一个值得我们探索的问题:在浏览器运行环境中,哪个语言的性能更优越,JavaScript 还是 WebAssembly?

笔者最近在工作中正好面临了这样的选择,我需要在浏览器运行时动态插入一些策略,用于在用户的浏览器运行时实现一些安全功能,例如网站请求的 CSRF 防护,网站存储数据的加解密等等,那么这种动态的运行时策略到底该使用 JavaScript 还是 WebAssembly 呢,为了找到答案,我做了一些验证,本文将详细对比两者在各项性能指标上的表现。

基础概念

  • JavaScript,诞生于 1995 年的一种高级编程语言,最初用于在 Web 浏览器中添加交互式元素。互动效果如弹出新的窗口,响应按钮点击,改变网页内容等,几乎都离不开 JavaScript。它的核心设计理念是"简单易懂”,语言本身易于上手,对新手友好。随着 Node.js 的出现,JavaScript 已不仅限于前端开发,而是成为一种全栈编程语言。
  • WebAssembly,或者简称 Wasm,是一种在浏览器环境下执行的新型二进制指令集,这就让浏览器拥有了执行其他代码(如 C、C++、Rust、Java)的能力。相较于JavaScript 的文本格式,WebAssembly 以二进制格式表达代码,使得其具有较高的执行效率。WebAssembly 是为了满足对高性能和低级功能的需求而产生的,比如游戏,音频视频编辑等。与 JavaScript 一样,Wasm 可以在几乎所有现代浏览器中运行。

测试代码

JavaScript

我们首先添加一个用于测试密集 CPU 计算的 cycle 函数,其他按照安全策略格式增加 20 个其他的函数(用于测试体积)。

代码语言:javascript
复制
window.StrategySet = {
    cycle: {
        key: 'cycle',
        name: '循环计算测试',
        expression: function (n) {
            let result = 0;
            for (let i = 0; i < n; i++) {
                result += i;
            }
            return result;
        }
    },
    detectTextHttp: {
        key: 'detectTextHttp',
        name: '检测网页明文传输请求',
        expression: function (event) {
            const { payload } = event;
            if (payload.url.startsWith('http://')) {
                console.log({ action: "REPORT_ONLY", reason: "HTTP REQUEST" }, event);
            } else {
                console.log({ action: "PASS", }, event);
            }
        }
    }
    // 剩余 20 个函数 ...
}

window.Strategys = StrategyGroup = {
    NETWORK_RESOURCE_REQUEST: ['detectTextHttp'],
    PAGE_INITIALIZED: ['fibonacci'],
    NETWORK_XHR_REQUEST: [],
    API_LOCALSTORAGE_GET: ['cycle'],
    API_CLIPBOARD_READ: [],
}

WebAssembly(Rust)

JS 实现完全一样的逻辑:添加一个用于测试密集 CPU 计算的 cycle 函数,其他按照相同的格式增加 20 个函数。

代码语言:javascript
复制

#[no_mangle]
pub fn cycle(n: u64) -> u64 {
    let mut result = 0;
    for i in 0..n {
        result += i;
    }
    result
}

struct DetectTextHttp {
    key: &'static str,
    name: &'static str,
    expression: Box<dyn Fn(Event)>,
}

struct Event {
    payload: Payload,
}

struct Payload {
    url: String,
}

impl DetectTextHttp {
    fn new(key: &'static str, name: &'static str, expr: Box<dyn Fn(Event)>) -> DetectTextHttp {
        DetectTextHttp {
            key: key,
            name: name,
            expression: expr,
        }
    }
}

fn main() {
    let detect_text_http = DetectTextHttp::new(
        "detectTextHttp",
        "检测网页明文传输请求",
        Box::new(|event: Event| {
            if event.payload.url.starts_with("http://") {
                println!("{{ action: \"REPORT_ONLY\", reason: \"HTTP REQUEST\" }}, {:?}, event");
            } else {
                println!("{{ action: \"PASS\" }}, event");
            }
        }),
    );
    
    // 剩余 20 个检测规则 ...
}

资源体积

JavaScript

JavaScript 在未经过任何压缩的情况下,代码体积为 1.8KB

WebAssembly(Rust)

Rust 源代码行数为 259 行,使用 cargo build --target wasm32-unknown-unknown 打包为 wasm 代码,最终网页中的加载的体积为 1.7MB

但这个是未经过任何优化和压缩的代码,我们使用 Rust 编译参数对产物的编译体积进行优化后:

代码语言:javascript
复制
   [profile.release]
   opt-level = 'z'  # 代码大小最小化
   lto = true       # 启用链接时优化,可以减少代码体积
   panic = 'abort'  # 抛弃默认的包含堆栈展开的恐慌处理器

最终压缩后的代码大小为 4.6KB:

所以,在同样的代码情况下,浏览器中可执行的代码文件体积上 JavaScript 更胜一筹。

代码初始化

因为是需要动态执行的策略,代码需要有一个动态拉取的过程,而不能直接打包在业务代码内部。

我们先添加一个测试的 HTML

代码语言:javascript
复制
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebAssembly 测试</title>
    <!-- 基础 SDK -->
    <script id="" src="./basic.js"></script>
    <!-- 一个外部 CDN JS -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
    <!-- 一个内联的 Script 脚本 -->
    <script>
        // 调用 localStorage API,触发 localStorage Hook
        localStorage.setItem('name', 'ConardLi');

        // 调用 fetch API ,触发 fetch Hook
        fetch('https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js');

    </script>
</head>

<body>
    <img src="https://pic0.sucaisucai.com/11/50/11050520_2.jpg">
</body>

</html>

然后我们在基础 SDK 中添加一些运行时的 Hook:

代码语言:javascript
复制
function initHook() {
    const observer = new PerformanceObserver((list) => {
        for (const entry of list.getEntries()) {
            log('[Hook] PerformanceObserver:', entry.name, window.Strategys);
        }
    });
    observer.observe({ entryTypes: ['resource'] });

    var originalFetch = window.fetch;
    window.fetch = function () {
        log('[Hook] Fetch:', arguments, window.Strategys);
        return originalFetch.apply(this, arguments);
    };

    var originalSetItem = localStorage.setItem;
    localStorage.setItem = function (key, value) {
        log('[Hook] localStorage.setItem:' + key, window.Strategys);
    };
}

JavaScript

策略拉取逻辑:

代码语言:javascript
复制
async function initStrategy() {
    log('[initStrategy] start download')
    document.write(`
        <script 
            src="https://lf3-static.bytednsdoc.com/obj/eden-cn/kyhuvjeh7pxpozps/snoopy/security-strategy4.js" 
            onload="console.log('动态策略已经加载并执行完毕!', performance.now() - window.time)">
        </script>`
    );
    log('[initStrategy] end download', performance.now() - window.time)
}

可见拉取、解析策略共花费的时间为 34ms,且后续同步执行的 JavaScript Hook 都可以拿到策略:

WebAssembly(Rust)

策略拉取逻辑(执行 WebAssembly 前还需要进行 ArrayBuffer 的转换、实例创建等流程,均为异步动作):

代码语言:javascript
复制
async function initStrategy() {
    log('[initStrategy] start download')
    const response = await fetch('https://lf3-static.bytednsdoc.com/obj/eden-cn/kyhuvjeh7pxpozps/snoopy/security-strategy1.wasm');
    log('[initStrategy] end download', performance.now() - window.time)
    const buffer = await response.arrayBuffer();
    log('[initStrategy] to arrayBuffer', performance.now() - window.time)
    const module = await WebAssembly.instantiate(buffer);
    log('[initStrategy] WebAssembly.instantiate', performance.now() - window.time)
    const cycle = module.instance.exports.cycle;
    window.Strategys = cycle;
}
  • 从开始到资源下载完成花费 142ms
  • ArrayBuffer 数据结构转换花费 363ms
  • WebAssembly 实例化花费 23ms

从开始拉取 WebAssembly 模块到最终可执行策略共消耗 528ms 。然后使用进行编译体积优化后的模块进行测试:

  • 从开始到资源下载完成花费 75ms
  • ArrayBuffer 数据结构转换花费 242ms
  • WebAssembly 实例化花费 24ms

整个过程均为异步,在这段时间页面上下载并解析的 JS 还是会继续执行的,在这期间 Hook 点位上拿不到策略。

长任务测试

为了让这段异步下载的过程更加直观,在业务代码中模拟一个纯 CPU 计算的长任务:

代码语言:javascript
复制
    <script>
        // 模拟一个长任务,用于体现策略拉取的异步动作
        console.log('[业务] start cpu', performance.now() - window.time);
        for (let i = 0; i < 999999999; i++) {
        }
        console.log('[业务] end cpu', performance.now() - window.time);
    </script>

可见 WebAssembly 模块实例化一定在业务长任务执行完后执行:

JavaScript 则会先解析好策略后再开始执行后续的 Script 逻辑:

代码执行

JavaScript

测试代码,调用 cycle 函数:

代码语言:javascript
复制
log('[initStrategy] 策略计算性能测试 init', performance.now() - window.time);

const result = window.StrategySet[window.Strategys['API_LOCALSTORAGE_GET'][0]].expression(999999999);

log('[initStrategy] 策略计算性能测试 end', result, performance.now() - window.time);

WebAssembly(Rust)

测试代码,调用 cycle 函数:

代码语言:javascript
复制
async function initStrategy() {
    log('[initStrategy] start download')
    const response = await fetch('https://lf3-static.bytednsdoc.com/obj/eden-cn/kyhuvjeh7pxpozps/snoopy/security-strategy1.wasm');
    log('[initStrategy] end download', performance.now() - window.time)
    const buffer = await response.arrayBuffer();
    log('[initStrategy] to arrayBuffer', performance.now() - window.time)
    const module = await WebAssembly.instantiate(buffer);
    log('[initStrategy] WebAssembly.instantiate', performance.now() - window.time)
    const cycle = module.instance.exports.cycle;
    window.Strategys = cycle;
    console.time('策略计算性能测试')
    const result = cycle(999999999n);
    log('[initStrategy] 策略计算性能测试', result, performance.now() - window.time);
    console.timeEnd('策略计算性能测试')
}

最终结论

  • 代码体积:
    • JavaScript:1.8KB ✅ VS WebAssembly(Rust) 4.6KB ❌
  • 初始化时间:
    • JavaScript:34ms ✅ VS WebAssembly(Rust) 528ms ❌
  • 10亿次循环代码执行时间:
    • JavaScript:1.3s ❌ VS WebAssembly(Rust) 0.1 ms ✅
  • JavaScript:首屏加载快、可同步加载、计算性能差:需要在业务首屏渲染前执行的策略、计算逻辑简单的策略,优先考虑使用 JavaScript 执行,例如 CSRF 防护、API 调用鉴权等策略。

  • WebAssembly:首屏初始化慢、只能异步加载、计算性能好:可以在业务首屏渲染完成后异步执行的策略,计算逻辑非常复杂、有密集 CPU 计算的策略,考虑使用 WebAssembly 模块执行,例如需要给业务图片在前端增加水印,需要对图片数据进行重写等策略。
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-01-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 code秘密花园 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 基础概念
  • 测试代码
    • JavaScript
      • WebAssembly(Rust)
      • 资源体积
        • JavaScript
          • WebAssembly(Rust)
          • 代码初始化
            • JavaScript
              • WebAssembly(Rust)
                • 长任务测试
                • 代码执行
                  • JavaScript
                    • WebAssembly(Rust)
                    • 最终结论
                    相关产品与服务
                    腾讯云服务器利旧
                    云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档