Nodejs 从 Webpack 到 Snowpack, 编译速度提升十倍以上——TRPG Engine 迁移小记

Nodejs 从 Webpack 到 Snowpack, 编译速度提升十倍以上——TRPG Engine 迁移小记

原文链接: http://moonrailgun.com/posts/74598ef5/

动机

TRPG Engine经过长久以来的迭代,项目已经显得非常臃肿了。数分钟的全量编译, 每次按下保存都会触发一次10s1m不等的增量编译让我苦不堪言, 庞大的依赖使其每一次编译都会涉及很多文件和很多包,长时的编译时间大大降低了开发效率与迭代速度。

优化方式

经过一段时间的考察,我选择了Snowpack作为解决方案。与Webpack不同的是,除了第一次的全量编译以外,Snowpack的增量编译不会涉及到庞大的node_modules文件夹, 准确来说只会编译变更文件本身。甚至于如果没有对依赖进行变更,下次的全量编译会直接动用之前编译的文件缓存,不需要花时间等待node_modules的编译。

为什么会这么快?这是由于Snowpack本身的实现与设计哲学有关的。相比Webpack, Snowpack利用了现代浏览器的本身的module系统,跳过复杂的模型之间的组织编译过程而只关注于变更文件本身的编译,这样当然快了。

Snowpack官方的一张图来说:

snowpack的最小编译单位是文件,而webpack的最小编译单位为chunk, 而chunk还需要额外的计算, 不论是编译部分还是编译后的组装部分。snowpack 的设计逻辑天生决定了她的速度。

优化前(使用webpack):

全量编译:

增量编译:

全量请求用时:

优化后(使用snowpack):

全量编译:

增量编译:

(看不到编译用时,但是体感在 1s 内. 而且该效果在电脑运行其他应用时更加显著)

全量请求用时:

  • 使用 http1

  • 使用 http2

以上测试是保证电脑在空闲时间,且保存与操作内容为同一文件

该用时已经是平时操作的最快时间,为此我的 MBR 重启了一次强制清空了 swap 空间, 实际表现会更加显著

因为文件依赖于浏览器的耗时,而浏览器需要串行请求依赖,因此耗时会更加长

但实际使用中使用 snowpack 会更加优秀。因为其相比 webpack 会大大节约电脑资源。在 webpack 编译时会占用大量的电脑资源,会影响到其他操作

遇到的坑与解决方案

TRPG Engine算是非常经典的Webpack应用了, 使用了各种 Loader 。光通用配置就有 250+行,各种优化配置,各种 alias 。等等长时间迭代积攒下来的配置,因此毫不意外的会遇到很多问题与坑。

以下是我遇到的问题与解决方案:

  • 问题 1:
    • 入口文件使用的是HtmlwebpackPlugin编译的handlebars文件,而 snowpack 不支持handlebars文件作为入口
    • 解决方案:重写一个snowpack专用的入口文件。使用handlebars主要解决的是 dll 的问题,snowpack不需要处理这部分的优化因此直接跳过
  • 问题 2:
    • snowpack加载文件策略与 node 不同。有同名文件和文件夹会优先使用文件夹的 index.js 作为路径解析。具体看现象可以参考这个讨论: https://github.com/snowpackjs/snowpack/discussions/1320
    • 解决方案:改名字,让文件夹与文件名不会出现重复。包括同名但是大小写不同的问题,因为底层是nodefs.stat实现,在大小写敏感的系统下依旧会视为同名
  • 问题 3:
    • TRPG Engine不但有 web 端,还有react-native端,而react-native是无法被正常解析的。我只想要处理 web 端的开发环境使用snowpack优化开发体验
    • 解决方案: exclude配置手动过滤
  • 问题 4:
    • tspath 不支持,虽然有了[@snowpack](/user/snowpack)/plugin-typescript但是不支持 tspath 。
    • 解决方案: 手动写了一个自动解析的逻辑将其变成对应的alias加到配置上
  • 问题 5:
    • 在 css 中引入了字体文件,但是无法正常加载。因为 snowpack 无法正确识别 url 指定的资源并将其打包(webpack 是使用css-loader来实现的)
    • 解决方案:
      scripts: {
        'mount:font': 'mount src/web/assets/fonts --to /main/fonts',
      },
      
  • 问题 6::
    • 对于一些特殊的写法我不想影响 webpack 的实现但是snowpack不支持这种写法。比如使用externals实现的配置引入, 比如DefinePlugin实现的process.env(在 snowpack 中必须使用import.meta.env), 再比如require的使用
    • 解决方案: 我实现了一个snowpack-plugin-replace插件用于将这些东西全部替换成我想要的代码。具体使用如下:
      [
        'snowpack-plugin-replace',
        {
          list: [
            {
              from: /process\.env/g,
              to: 'import.meta.env',
            },
            {
              from: `require("../../package.json").version`,
              to: '"0.0.0"',
            },
            {
              from: `const resBundle = require("i18next-resource-store-loader!./langs/index.js");`,
              to: 'import resBundle from "./langs/zh-CN/translation.json"',
            },
            {
              from: 'import Config from "config";',
              to: `const Config = ${JSON.stringify({
                sentry: require('config').get('sentry'),
              })};`,
            },
          ],
        },
      ],
      
  • 问题 7:
    • rollup 抛出无法解析this的警告
    • 解决方案: 使用 context 指向 window 来移除警告
      installOptions: {
        rollup: {
          context: 'window',
        },
      },
      
  • 问题 8:
    • snowpack 打包目标路径与原有的 build 文件夹冲突
    • 解决方案: 修改输出目录为.snowpack并在 gitignore 中添加该文件夹
      devOptions: {
        out: '.snowpack',
      },
      
  • 问题 9:
    • 使用[@snowpack](/user/snowpack)/plugin-typescript内部包对全局变量的声明会出现重复声明的报错
    • 解决方案: tsconfig"skipLibCheck": true
  • 问题 10:
    • 现有的依赖需要[@babel](/user/babel)/plugin-transform-runtime提供的helpers作为全局依赖
    • 解决方案: 经检查是用到了regenerator功能,手动安装regenerator-runtime并在包前引入import 'regenerator-runtime/runtime';
  • 问题 11:
    • 部分依赖在其中部分代码使用了require作为引入方式, 而snowpack无法正确处理require
    • 解决方案: 检查后发现都已经修改。升级依赖到最新版即可
  • 问题 12:
    • 在使用 less 的 import 逻辑无法正常运行,这是由于snowpack的具体实现决定的。
    • 暂时无法解决,使用snowpack-plugin-replace将其替换为 css 文件导入作为临时解决方案, 见讨论: Github

总结

Snowpack 虽然作为一个新兴的打包工具,目前尚不是非常完善, 功能也没有 webpack 这样丰富与齐全。但是它的新的打包设计对于有一定规模的前端应用还是非常优秀的。能极大提升开发效率。不失为一种好的解决方案。当然最后输出还是需要使用 webpack 对其进行一定的优化,毕竟原生的 module 支持目前浏览器的支持度还没有达到覆盖一个理想的地步https://caniuse.com/es6-module

最后这是我最后提交的pr


17 回复

说实话看到下面这么多遇到的问题 还是保持观望
不过涨姿势了 之前只知道 esbuild 才知道还有 Snowpack


上次我也一顿猛操换成这个,碰到一堆问题,后来又退回去了。印象中还有 commonjs 格式导出的模块也是不能用的。感觉还是要等个一年各个模块兼容性上去之后再换比较合适。

这个就没有 typescript 类型检查了吧?

Webpack 换 Parcel 那一波已经冲过了.结果很失望.

现在碰到所有新东西都先观望观望了.

有的。可以选择 babel 插件的 ts 模块或者 typescript 插件(只能二选一)

使用 typescript 插件的的话其实就是后台运行一个 tsc 的 watch 模式

parcel 本质没有太大的变化,反而少了 webpack 的大量配置带来的优势与劣势。

我换 snowpack 目的是为了提升开发体验。我现在按一次保存按钮电脑十几秒不能动 ide 也没有提示。算是比较有目的性的。

生产环境还是会使用 webpack 进行代码的优化与编排。目前尚没有到可以完全替换的地步

#6 嗯.我一向认同新工具的优势,但是大部分的成熟程度都没有到支撑大型项目的程度.我个人博客用 Gatsby,但是公司项目我连 TS 都没上.不想挖坑给自己填,所以还是算了

几个月前我也试过。也是一堆问题,改了改源码也没法解决差太多了,就没继续搞。最近重新弄了一下发现还是有点盼头的。虽然遇到一堆问题但是基本上都解决或者找到替代方案。

做大动作前一定要分分支,这样万一搞不成直接扔了就好了

公司项目还是谨慎一点比较好。毕竟不同人不断迭代下肯定会出现很多奇奇怪怪的 magic,如果是个人项目能够控制全局,我觉得还是可以考虑实际根据情况尝试一下的。

和 webpack5 比较怎么样?

我也是用从 webpack 换成了 rollup,速度也是快了很多,https://github.com/zy445566/before-server/commit/4d61a3b0c8464eb8d08605178fdaebc703db7599

有大量库及其依赖库使用 CommonJS 导入导出,这块是最难解决的。就算有一直活跃的库修改为 ESModule, 保不齐上级深层的某个依赖万年不修,依旧用的 CommonJS,那 Snowpack 就没法用了。

vue3 的话可以直接 vite 了 不过踩了这么多坑 可以收藏 以后 自己玩的时候 可以来看看


snowpack 内部通过 rollup 来实现打包。通过 commonjs-plugin 实现对 commonjs 格式的兼容

具体见: https://github.com/snowpackjs/snowpack/blob/d90a1fb8a080bfe32e7283d87063381cd97f48bb/esinstall/src/index.ts#L383-L387

不过实际使用过程中如果依赖使用了行内 require 会无法正常执行。我的实际 case 中遇到了两处。翻阅代码发现已经被修复了因此升级就解决了

我遇到一处行内 require 的问题,查看依赖代码,发现不予解决,那就无解了。有这类问题的肯定不止一个包,,而且这问题看似简单,一旦出现,你的开发环境就起不来了。
https://github.com/antvis/g/pull/598

这个不是行内 require 的问题,而是在 esmodule 中使用 commonjs 的语法引入了一个 json 内容的问题。

按照标准来说这种行为属于混用,统一使用 esmodule 或者统一使用 commonjs 都不会出现这个问题。

我觉得不支持标准行为属于正常操作。本质上来说内部的 rollup 没有处理这种情况,snowpack 自身也不会去处理,也就 webpack 这种大而全的会考虑方方面面兼容一切 case——这也是 webpack 显得臃肿的原因

针对您提到的Node.js项目从Webpack迁移到Snowpack,编译速度提升显著的问题,这里提供一些专业见解。

TRPG Engine项目在长期使用Webpack后,编译过程变得非常臃肿,全量编译需要数分钟,每次保存操作都会触发10秒到1分钟不等的增量编译,大大降低了开发效率和迭代速度。迁移到Snowpack后,除了首次全量编译,增量编译不再涉及庞大的node_modules文件夹,只关注变更文件本身的编译,从而大大提升了编译速度。

Snowpack利用了现代浏览器的ES Modules系统,跳过了Webpack复杂的模块组织编译过程。Snowpack的最小编译单位是文件,而Webpack的最小编译单位为chunk,需要额外的计算。这种设计逻辑使得Snowpack天生具备更快的编译速度。

迁移过程中,可能会遇到一些问题,如入口文件处理、文件加载策略差异、React Native端无法解析等。以下是一个简单的示例,展示如何在Snowpack中处理CSS中的字体文件:

// snowpack.config.json
{
  "scripts": {
    "mount:font": "mount src/web/assets/fonts --to /main/fonts"
  }
}

此外,对于一些特殊的写法,如使用externals实现的配置引入、DefinePlugin实现的process.env等,在Snowpack中可能不支持,需要手动实现插件进行替换。

总的来说,从Webpack迁移到Snowpack可以显著提升编译速度,优化开发体验。但迁移过程中需要处理一些兼容性问题,并可能需要手动实现一些插件来适配原有的项目配置。希望这些信息对您有所帮助。

回到顶部