性能文章>如何使用 Webpack 将启动时间减少 80%>

如何使用 Webpack 将启动时间减少 80%转载

2年前
420001

导语

webpack的性能调优是前端优化经常用的优化方式,本篇主要通过对webpack的配置来优化webpack,比较适合初级读者阅读。

 

正文

我们在 RudderStack 使用的开发方式之一是安全快速地构建,然后根据需要进行优化,这种模式使我们能够优先考虑客户问题,跟上 RudderStack 的快速增长的脚步。


但在某些情况下,这种方式会导致开发体验的流失。发生这种情况时,我们使用帕累托原则重新集中精力,力求在消除技术债务中投入的时间能得到最大的回报。


这种不太好的开发体验的一个例子是 Control Plane 的主后端服务的部署时间过长。过去在生产环境中部署需要 5 分钟,更甚的是,在开发过程中,根据硬件的不同,重启需要 40-90 秒,这成了一个主要的痛点,拖慢了我们团队的进度,我们知道,是时候重新关注和解决它了,我们是这样做的。


Control Plane 是什么?


首先,我解释一下我所说的“Control Plane(控制台)”,Rudderstack 的架构分为两部分:数据台和控制台。控制台是 Rudderstack 平台的大脑,它是存储资源和配置的地方,你的组织、工作区、基础设施和账单中的用户管理和协作都在控制台中进行。

如何使用 Webpack 将启动时间减少 80%数据图表-heapdump性能社区


从架构的角度来看,控制台由一个以集群模式运行的后端应用、几个附属微服务和一个前端应用组成。对于我们的后端服务,我们使用 Node.js 和 Typescript,用 ts-node 来启动和运行应用程序。但是如上所述,这是有代价的,让我们深入了解里面发生了什么。


解决我们启动时间的问题


我们知道 Node.js 不是问题的原因,原生的 HTTP 服务器几乎是立即重启,我们使用的 koa web 框架精简且轻量级。所以,我们需要做一些分析来查明原因,使用 clinic.js 来帮助分析,它简单而易用。


果然,在设置好 clinic 并进行了几次测试运行之后,我们生成了一些火焰图(火焰图是一种显示每个方法和依赖项需要多少执行(CPU)时间的方式),它们揭示了问题。


带有源代码和过程的火焰图:

如何使用 Webpack 将启动时间减少 80%数据图表-heapdump性能社区


没有源代码的过程火焰图:

如何使用 Webpack 将启动时间减少 80%数据图表-heapdump性能社区


不管是否包含 rudder-config-backend 源代码,图表都是一样的,所以我们知道源代码不是问题,并且可以确定开销来自 Typescript,尤其是 ts-node。


这是有道理的,因为每当进程重新启动时,整个源代码都必须从零开始转换为 Javascript,而且没有任何缓存;这与我们在集群模式下部署服务器时遇到的较大延迟一致。每个工作进程都必须独立编译 Typescript 文件,因此重新启动需要很多时间,有时还会导致资源匮乏。具体来说,我们在服务器启动期间,可以看到内存不足错误和 CPU 利用率在增加。


虽然在生产中使用 ts-node 并不是一种坏的做法 (如果设置得当),但在我们的案例中,我们意识到它会产生大量的开销,然而我们严重依赖 TypeORM 和 reflect-metadata,这使得 ts-node 很有吸引力。消除这种依赖需要大量的工作,并可能通过限制我们的工具集而导致 DX 的进一步退化。所以,我们只有一个选择:删除 Typescript。


当然,不是完全删除 Typescript,只是在生产环境。至少在理论上,让一个 node 进程加载.js 文件,而不是用 ts-node 包装器,这将大大减少启动时间,正如我们在第二个火焰图中观察到的那样。当然,我们可以采取不同的方法来实现这一点,但每一种方法都有利弊。


方法一:使用 tsc


我们最初的方法是使用 tsc 二进制文件,和安装的 Typescript 版本一起打包,并增加一个编译步骤。事实证明,这比想象的更棘手,因为几位工程师在 2 年多的时间里用不同的方法开发了配置的后端。因此,我们遇到了一些问题:

  • 多个依赖项用了不同的模块,tsc 一次只能处理一种方式。
  • Typescript 输出一个真实的、一对一的源到分发目录、使用了不同格式的 imports —— 有些是相对于 package.json,有些是别名。
  • Typescript 在设计上不会修改依赖项的导入路径,带有模块的 Node.js 对文件名应该如何表示有严格的要求。

方法二:用 ttypescript 和 ttsc 扩展 Typescript


可以使用几个补丁来修改 tsc 的行为,绕过 Typescript 的转译限制。不幸的是,这些解决方案虽然不是很复杂,但需要需要大量的混合和匹配来覆盖所有用例,并且对项目添加了额外的依赖项,例如 typescript-transformer-append-js-extension。


退一步说,我们意识到将不得不牺牲 Typescript 模块提供的一些便利,并重写应用程序的某些部分,尤其是在导入模块方面。


但是,如果有一个解决方案可以找出依赖关系,以及如何以声明的方式导入它们呢?


进入 webpack


webpack 是一个传统的 JavaScript 模块打包器,创建的目的是通过有效地将前端应用分割成块,快速地将其传送到用户的浏览器。作为最古老、最成熟的打包工具之一,至今仍在积极地维护中,webpack 拥有一个庞大的插件生态系统,适应任何类型的复杂应用,并且它对 Node.js 提供了一流的支持。


由于 webpack 就是为此目的而构建的,让它来处理模块解析和转换.ts 文件,相比其它类 hack 和猴子补丁方法,感觉更自然。我们努力了几次让 webpack 与 TypeORM 一起工作,主要是因为 TypeORM 顽固的设定。例如,数据库迁移文件必须在类名末尾包含时间戳,这意味着源文件不能缩小,导入 / 导出名称不能被篡改。但经过几次尝试,我们成功了。果然,通过 webpack 及其插件处理,每个文件都简化了构建过程。通过高效缓存,后续构建的速度会更快,从而获得更好的 DX 和更短的部署窗口。集群模式的部署现在大约需要 12 秒,缩短了近 5 分钟!——从服务请求开始。请记住,这是 8 个节点进程共享的资源,每个节点进程启动一个 koa 的 web 服务器和通过 TypeORM 连接到数据库。


在开发过程中,结果更加突出:

如何使用 Webpack 将启动时间减少 80%数据图表-heapdump性能社区

以下是我们用来大幅减少启动时间的 webpack 配置:


安装需要的依赖:

npm install --save-dev webpack webpack-cli @types/webpack-env


webpack 和 webpack-cli 不言自明,第三个包 @types/webpack-env,会启用 webpack 的 require.Context 的自动完成功能,这需要手动指导 webpack 如何以元编程的方式处理符号,例如,在源代码目录中找到你的 ORM 实体并自动声明它们,而不是专门地一个个导入——我们有大量这样的实体!


注意:所有这些依赖项只能在开发和构建期间使用,不需要在生产构建中加载它们!


创建和导出配置文件
webpack 的配置非常简单,只需在你的项目根目录(通常是 package.json 所在的文件夹)中创建一个 webpack.config.js 文件,然后导出 webpack 配置。它看起来可能像这样:

module.exports = {
// webpack config
}


添加构建入口和路径

 

module.exports = {
  entry: './src/index.ts',  // the file you would provide to ts-node or node binaries for execution
  mode: NODE_ENV,  // development or production
  target: 'node',  // webpack works differently based on target, here we use node.js
  output: {  // directions for the built files directory
  path: path.resolve(__dirname, 'dist'),
  filename: 'index.js',
  },
}


配置如何查找源代码文件

module.exports = {
  // ...
  resolve: {
  // Bundle only typescript files
  extensions: ['.ts'],
  alias: {
    // provide any import aliases you may use in your project
    src: path.resolve(__dirname, 'src/'),
    '@controller': path.resolve(__dirname, 'src/controllers/'),
    '@service': path.resolve(__dirname, 'src/services/'),
  },
  },
}


配置读取 ts 文件


对于这一步,你可以安装任何你喜欢的 webpack 的 typescript 加载器。我们使用 ts-loader:

npm install --save-dev ts-loader

 

module.exports = {
  // ...
  module: {
  rules: [
    {
      test: /\.ts$/,   // this rule will only activate for files ending in .ts
      use: [{ loader: 'ts-loader' }],  
      exclude: [  // exclude any files you don't want to include
        /__tests__/,
      ],
    },
  ],
  },
}


添加外部扩展,这样 webpack 就不会打包外部依赖(node 模块)

npm install --save-dev webpack-node-externals

 

module.exports = {
  // ...
  externals: [nodeExternals()],
}


别忘了你的插件——webpack 一切与插件相关!

module.exports = {
  // ...
  plugins: [
    ...plugins,
  ],
}


下面是我们使用的一些插件的列表,向出色的贡献者和维护者致敬!

  • nodemon-webpack-plugin:nodemon 的标准包装器,使开发速度更快。
  • webpack-shell-plugin-next:添加构建生命周期钩子来运行 cli 命令,例如,在构建源文件之前构建 swagger 文件。
  • fork-TS-checker-webpack-plugin:在一个独立进程上运行 TS 类型检查器,以提高构建期间的性能。注意:如果你使用这个,请确保更新步骤 5 中的 module.rules.use 为:{loader: 'ts-loader', options: {transpileOnly: true}},这样 ts-loader 就不会运行类型检查。

最终的 webpack 配置


你最终的 webpack 配置应该是这样的:

const path = require('path');
const nodeExternals = require('webpack-node-externals');

const {
  NODE_ENV = 'production',
} = process.env;

module.exports = {
  entry: './src/index.ts',  // the file you would provide to ts-node or node binaries for execution
  mode: NODE_ENV,  // development or production
  target: 'node',  // webpack works differently based on target, here we use node.js
  output: {  // directions for the built files directory
  path: path.resolve(__dirname, 'dist'),
  filename: 'index.js',
  },
  resolve: {
  // Bundle only typescript files
  extensions: ['.ts'],
  alias: {
    // provider any import aliases you may use in your project
    src: path.resolve(__dirname, 'src/'),
    '@controller': path.resolve(__dirname, 'src/controllers/'),
    '@service': path.resolve(__dirname, 'src/services/'),
  },
  },
  module: {
  rules: [
    {
      test: /\.ts$/,   // this rule will only activate for files ending in .ts
      use: [{ loader: 'ts-loader' }],
      exclude: [  // exclude any files you don't want to include, eg test files
        /__tests__/,
      ],
    },
  ],
  },
  externals: [nodeExternals()],
  plugins: [
  // any plugins you may find useful
  ],
}


优化为更多的优化铺平道路


我们从运行时的依赖项中删除了 Typescript,所以我们在最终的生产制品中不再需要它,这样我们完全摆脱了这些依赖!


它也启发我们优化了构建流水线,通过引入带缓存层、和为开发和生产不同目标的多阶段 docker 构建,使其更为高效。


更少的依赖意味着:

  • 更小的图像尺寸。
  • 减少第三方代码造成的内存泄漏的机会。
  • 更少的带宽使用。
  • 更快的传输时间。


最重要的是,它意味着面临更少的攻击,由于依赖更少、审计和解决漏洞的时间更少,让 RudderStack 对我们的客户来说更加安全。

 

更多思考

优化webpack的方法有很多,本篇主要详细解读了一种webpack优化的一种方式更多关于webpack的优化大家可以阅读以下内容加深阅读:

用Webpack这15个点,速度提升70%,体积减小80%!

我的第一次webpack优化,首屏渲染从9s到1s

 

点赞收藏
分类:标签:
Meguro
请先登录,感受更多精彩内容
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步
1
0