Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

js打包时间缩短90%,bundleless生产环境实践总结 #72

Open
forthealllight opened this issue Aug 23, 2021 · 2 comments
Open

Comments

@forthealllight
Copy link
Owner

forthealllight commented Aug 23, 2021

js打包时间缩短90%,bundleless生产环境实践总结


    最近尝试将bundleless的构建结果直接用到了线上生产环境,因为bundleless只会编译代码,不会打包,因此构建速度极快,同比bundle模式时间缩短了90%以上。得益于大部分浏览器都已经支持了http2和浏览器的es module,对于我们没有强兼容场景的中后台系统,将bundleless构建结果应用于线上是有可能的。本文主要介绍一下,本人在使用bundleless构建工具实践中遇到的问题。

  • 起源
  • 结合snowpack实践
  • snowpack的Streaming Imports
  • 性能比较
  • 总结
  • 附录snowpack和vite的对比

一、起源

1.1 从http2谈起

    以前因为http1.x不支持多路服用, HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8个的TCP链接请求限制.因此我们需要做的就是将同域的一些静态资源比如js等,做一个资源合并,将多次请求不同的js文件,合并成单次请求一个合并后的大js文件。这就是webpack等bundle工具的由来。

    而从http2开始,实现了TCP链接的多路复用,因此同域名下不再有请求并发数的限制,我们可以同时请求同域名的多个资源,这个并发数可以很大,比如并发10,50,100个请求同时去请求同一个服务下的多个资源。

因为http2实现了多路复用,因此一定程度上,将多个静态文件打包到一起,从而减少请求次数,就不是必须的

    主流浏览器对http2的支持情况如下:

Lark20210825-203949

    除了IE以外,大部分浏览器对http2的支持性都很好,因为我的项目不需要兼容IE,同时也不需要兼容低版本浏览器,不需要考虑不支持http2的场景。(这也是我们能将不bundle的代码用于线上生产环境的前提之一)

1.2 浏览器esm

    对于es modules,我们并不陌生,什么是es modules也不是本文的重点,一些流行的打包构建工具比如babel、webpack等早就支持es modules。

    我们来看一个最简单的es modules的写法:

//main.js
import a from 'a.js'
console.log(a)

//a.js
export let  a = 1

    上述的es modules就是我们经常在项目中使用的es modules,这种es modules,在支持es6的浏览器中是可以直接使用的。

    我们来举一个例子,直接在浏览器中使用es modules

<html  lang="en">
    <body>
        <div id="container">my name is {name}</div>
        <script type="module">
           import Vue from 'https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.esm.browser.js'
           new Vue({
             el: '#container',
             data:{
                name: 'Bob'
             }
           })
        </script>
    </body>
</html>

上述的代码中我们直接可以运行,我们根据script的type="module"可以判断浏览器支不支持es modules,如果不支持,该script里面的内容就不会运行。

    首先我们来看主流浏览器对于ES modules的支持情况:

Lark20201119-151747

    从上图可以看出来,主流的Edge, Chrome, Safari, and Firefox (+60)等浏览器都已经开始支持es modules。

    同样的因为我们的中后台项目不需要强兼容,因此不需要兼容不支持esm的浏览器(这也是我们能将不bundle的代码用于线上生产环境的前提之二)。

1.3 小结

    浏览器对于http2和esm的支持,使得我们可以减少模块的合并,以及减少对于js模块化的处理。

  • 如果浏览器支持http2,那么一定程度上,我们不需要合并静态资源
  • 如果浏览器支持esm,那么我们就不需要通过构建工具去维护复杂的模块依赖和加载关系。

这两点正是webpack等打包工具在bundle的时候所做的事情。浏览器对于http2和esm的支持使得我们减少bundle代码的场景。

二、结合snowpack实践

    我们比较了snowpack和vite,最后选择采用了snowpack(选型的原因以及snowpack和vite的对比看最后附录),本章节讲讲如何结合snowpack构建工具,构建出不打包形式的线上代码。

2.1 snowpack的基础用法

    我们的中后台项目是react和typescript编写的,我们可以直接使用snowpack相应的模版:

npx create-snowpack-app myproject --template @snowpack/app-template-react-typescript

snowpack构建工具内置了tsc,可以处理tsx等后缀的文件。上述就完成了项目初始化。

2.2 前端路由处理

    前端路由我们直接使用react-router或者vue-router等,需要注意的时,如果是在开发环境,那么必须要指定在snowpack.config.mjs配置文件,在刷新时让匹配到前端路由:

snowpack.config.mjs
...
  routes: [{ match: 'routes', src: '.*', dest: '/index.html' }],
...

类似的配置跟webpack devserver等一样,使其在后端路由404的时候,获取前端静态文件,从而执行前端路由匹配。

2.3 css、jpg等模块的处理

    在snowpack中同样也自带了对css和image等文件的处理。

  • css
    以sass为例,
snowpack.config.mjs

plugins:  [
     '@snowpack/plugin-sass',
     {
       /* see options below */
     },
   ],

只需要在配置中增加一个sass插件就能让snowpack支持sass文件,此外,snowpack也同样支持css module。.module.css或者.module.scss命名的文件就默认开启了css module。此外,css最后的结果都是通过编译成js模块,通过动态创建style标签,插入到body中的。

   //index.module.css文件
    
   .container{
       padding: 20px;
   }

snowpack构建处理后的css.proxy.js文件为:

export let code = "._container_24xje_1 {\n  padding: 20px;\n}";
let json = {"container":"_container_24xje_1"};
export default json;

// [snowpack] add styles to the page (skip if no document exists)
if (typeof document !== 'undefined') {
  const styleEl = document.createElement("style");
  const codeEl = document.createTextNode(code);
  styleEl.type = 'text/css';

  styleEl.appendChild(codeEl);
  document.head.appendChild(styleEl);
}

上述的例子中我们可以看到。最后css的构建结果是一段js代码。在body中动态插入了style标签,就可以让原始的css样式在系统中生效。

  • jpg,png,svg等

如果处理的是图片类型,那么snowpack同样会将图片编译成js.

//logo.svg.proxy.js

export default "../dist/assets/logo.svg";

    snowpack没有对图片做任何的处理,只是把图片的地址,包含到了一个js模块文件导出地址中。值得注意的是在浏览器es module中,import 动作类似一个get请求,import from可以是一个图片地址,浏览器es module自身可以处理图片等形式。因此在.js文件结尾的模块中,export 的可以是一个图片。

snowpack3.5.0以下的版本在使用css module的时候会丢失hash,需要升级到最新版本。

2.4 按需加载处理

     snowpack默认是不打包的。只对每一个文件都做一些简单的模块处理(将非js模块转化成js模块)和语法处理,因此天然支持按需加载,snowpack支持React.lazy的写法,在react的项目中,只要正常使用React.Lazy就能实现按需加载。

2.5 文件hash处理

    在最后构建完成后,在发布构建结果的时候,为了处理缓存,常见的就是跟静态文件增加hash,snowpack也提供了插件机制,插件会处理snowpack构建前的所有文件的内容,做为content转入到插件中,经过插件的处理转换后得到新的content.

    可以通过[snowpack-files-hash][1]插件来实现给文件增加hash。

2.6 公用esm模块托管

    snowpack对于项目构建的bundleless的代码可以直接跑在线上,在bundless的构建结果中,我们想进一步减少构建结果文件大小。以bundleless的方式构建的代码,默认在处理三方npm包依赖的时候,虽然不会打包,snowpack对项目中node_modules中的依赖重新编译成esm形式,然后放在一个新的静态目录下。因此最后构建的代码包含了两个部分:

项目本身的代码,将node_modules中的依赖处理成esm后的静态文件

    其中node_modules中的依赖处理成esm后的静态文件,可以以cdn或者其他服务形式来托管。这样我们每次都不需要在构建的时候处理node_modules中的依赖。在项目本身的代码中,如果引用了npm包,只需要将其指向一个cdn地址即可。这样处理后的,构建的代码就变成:

只有项目本身的代码(项目中对于三方插件的引入,直接使用三方插件的cdn地址)

    进一步想,如果我们使用了托管所有npm包(es module形式)的cdn地址之后,那么在本地开发或者线上构建的过程中,我们甚至不需要去维护本地的node_modules目录,以及yarn-lock或者package-lock文件。我们需要做的,仅仅是一个map文件进行版本管理。保存项目中的npm包名和该包相对应的cdn地址。

比如:

//config.map.json
{
  "react": "https://cdn.skypack.dev/react@17.0.2",
  "react-dom": "https://cdn.skypack.dev/react-dom@17.0.2",
}

通过这个map文件,不管是在开发还是线上,只要把:

import React from 'react'

替换成

import React from "https://cdn.skypack.dev/react@17.0.2"

就能让代码在开发环境或者生产环境中跑起来。如此简化之后,我们不论在开发环境还是生产环境都不需要在本地维护node_modules相关的文件,进一步可以减少打包时间。同时包管理也更加清晰,仅仅是一个简单的json文件,一对固定意义的key/value,简单纯粹

    我们提到了一个托管了的npm包的有es module形式的cdn服务,上述以skypack为例,这对比托管了npm包cjs形式的cdn服务unpkg,两者的区别就是,unpkg所托管的npm包,大部分是cjs形式的,cjs形式的npm包,是不能直接用于浏览器的esm中的。skypack所做的事情就是将大部分npm包从cjs形式转化成esm的形式,然后存储和托管esm形式的结果。

三、snowpack的Streaming Imports

    在2.7中我们提到了在dev开发环境使用了skypack,那么本地不需要node_modules,甚至不需要yarn-lock和package-lock等文件,只需要一个json文件,简单的、纯粹的,只有一对固定意义的key/value。在snowpack3.x就提供了这么一个功能,称之为Streaming Imports。

3.1 snowpack和skypack

    在snowpack3.x在dev环境支持skypack:

// snowpack.config.mjs
export default {
  packageOptions: {
    source: 'remote',
  },
};

    如此,在dev的webserver过程中,就是直接下载skypack中相应的esm形式的npm包,放在最后的结果中,而不需要在本地做一个cjs到esm的转换。这样做有几点好处:

  • 速度快: 不需要npm install一个npm包,然后在对其进行build转化成esm,Streaming Imports可以直接从一个cdn地址直接下载esm形式的依赖
  • 安全:业务代码中不需要处理公共npm包cjs到esm的转化,业务代码和三方依赖分离,三方依赖交给skypack处理

3.2 依赖控制

    Streaming Imports自身也实现了一套简单的依赖管理,有点类似go mod。是通过一个叫snowpack.deps.json文件来实现的。跟我们在2.7中提到的一样,如果使用托管cdn,那么本地的pack-lock和yarn-lock,甚至node_modules是不需要存在的,只需要一个简单纯粹的json文件,而snowpack中就是通过snowpack.deps.json来实现包的依赖管理的。

我们安装一个npm包时,我们以安装ramda为例:

npx snowpack ramda

在snowpack.deps.json中会生成:

{
  "dependencies": {
    "ramda": "^0.27.1",
  },
  "lock": {
    "ramda#^0.27.1": "ramda@v0.27.1-3ePaNsppsnXYRcaNcaWn",
  }
}

安装过程的命令行如下所示:

飞书20210831-211844

从上图可以看出来,通过npx snowpack安装的依赖是从skypack cdn直接请求的。

特别的,如果项目需要支持typescript,那么我们需要将相应的npm包的声明文件types下载到本地,skypack同样也支持声明文件的下载,只需要在snowpack的配置文件中增加:

// snowpack.config.mjs
export default {
  packageOptions: {
    source: 'remote',
    types:true //增加type=true
  },
};

snowpack会把types文件下载到本地的.snowpack目录下,因此在tsc编译的时候需要指定types的查找路径,在tsconfig.json中增加:

//tsconfig.json
"paths": {
      "*":[".snowpack/types/*"]
    },

3.3 build环境

    snowpack的Streaming Imports,在dev可以正常工作,dev的webserver中在请求npm包的时候会将请求代理到skypack,但是在build环境的时候,还是需要其他处理的,在我们的项目中,在build的时候可以用一个插件[snowpack-plugin-skypack-replacer][2],将build后的代码引入npm包的时候,指向skypack。

build后的线上代码举例如下:

import * as __SNOWPACK_ENV__ from '../_snowpack/env.271340c8a413.js';
import.meta.env = __SNOWPACK_ENV__;

import ReactDOM from "https://cdn.skypack.dev/react-dom@^17.0.2";
import App from "./App.e1841499eb35.js";
import React from "https://cdn.skypack.dev/react@^17.0.2";
import "./index.css.proxy.9c7da16f4b6e.js";

const start = async () => {
  await ReactDOM.render(/* @__PURE__ */ React.createElement(App, null), document.getElementById("root"));
};
start();
if (undefined /* [snowpack] import.meta.hot */ ) {
  undefined /* [snowpack] import.meta.hot */ .accept();
}

从上述可以看出,build之后的代码,通过插件将:

import React from 'react'
//替换成了
import React from "https://cdn.skypack.dev/react@^17.0.2";

四、性能比较

4.1 lighthouse对比

    简单的使用lighthouse来对比bundleless和bundle两种不同构建方式网页的性能。

  • bundleless的前端简单性能测试:

  • bundle的前端性能测试:

对比发现,这里两个网站都是同一套代码,相同的部署环境,一套是构建的时候是bundleless,利用浏览器的esm,另一个是传统的bundle模式,发现性能上并没有明显的区别,至少bundleless简单的性能测试方面没有明显差距。

4.2构建时间对比

    bundleless构建用于线上,主要是减少了构建的时间,我们传统的bundle的代码,一次编译打包等可能需要几分钟甚至十几分钟。在我的项目中,bundleless的构建只需要4秒。

飞书20210901-165311

    同一个项目,用webpack构建bundle的情况下需要60秒左右。

4.3构建产物体积对比

    bundleless构建出的产物,一般来说也只有bundle情况下的1/10.这里不一一举例。

五、总结

    在没有强兼容性的场景,特别是中后台系统,bundleless的代码直接跑在线上,是一种可以尝试的方案,上线的时间会缩短90%,不过也有一些问题需要解决,首先需要保证托管esm资源的CDN服务的稳定性,且要保障被托管的esm资源在浏览器运行不会出现异常。我们运行了一些常见的npm包,发现并没有异常情况,不过后续需要更多的测试。

六、附录:snowpack和vite的对比

6.1 相同点

    snowpack和vite都是bundleless的构建工具,都利用了浏览器的es module来减少对静态文件的打包,从而减少热更新的时间,从而提高开发体验。原理都是将本地安装的依赖重新编译成esm形式,然后放在本地服务的静态目录下。snowpack和vite有很多相似点

  • 在dev环境都将本地的依赖进行二次处理,对于本地node_module目录下的npm包,通过其他构建工具转换成esm。然后将所有转换后的esm文件放在本地服务的静态目录下
  • 都支持css、png等等静态文件,不需要安装其他插件。特别对于css,都默认支持css module
  • 默认都支持jsx,tsx,ts等扩展名的文件
  • 框架无关,都支持react、vue等主流前端框架,不过vite对于vue的支持性是最好的。

6.2 不同点

dev构建:
    snowpack和vite其实大同小异,在dev环境都可以将本地node_modules中npm包,通过esinstall等编译到本地server的静态目录。不同的是在dev环境

  • snowpack是通过rollup来将node_modules的包,重新进行esm形式的编译
  • vite则是通过esbuild来将node_modules的包,重新进行esm形式的编译
    因此dev开发环境来看,vite的速度要相对快一些,因为一个npm包只会重新编译一次,因此dev环境速度影响不大,只是在初始化项目冷启动的时候时间有一些误差,此外snowpack支持Streaming Imports,可以在dev环境直接用托管在cdn上的esm形式的npm包,因此dev环境性能差别不大。

build构建:

    在生产环境build的时候,vite是不支持unbundle的,在bundle模式下,vite选择采用的是rollup,通过rollup来打包出线上环境运行的静态文件。vite官方支持且仅支持rollup,这样一定程度上可以保持一致性,但是不容易解耦,从而结合非rollup构建工具来打包。而snowpack默认就是unbundle的,这种unbundle的默认形式,对构建工具就没有要求,对于线上环境,即可以使用rollup,也可以使用webpack,甚至可以选择不打包,直接使用unbundle。

可以用两个表格来总结如上的结论:

dev开发环境:

产品 dev环境构建工具
snowpack rollup(或者使用Streaming imports)
vite esbuild

build生产环境:

产品 build构建工具
snowpack 1.unbundle(esbuild) 2.rollup 3.webpack...
vite rollup(且不支持unbundle)

6.3 snowpack支持Streaming Imports

    Streaming Imports是一个新特性,他允许用户,不管是生产环境还是开发环境,都不需要在本地使用npm/yarn来维护一个lock文件,从而下载应用中所使用的npm包到本地的node_module目录下。通过使用Streaming Imports,可以维护一个map文件,该map文件中的key是包名,value直接指向托管该npm包esm文件形式的cdn服务器的地址。

6.4 vite的一些优点

    vite相对于snowpack有一下几个优点,不过个人以为都不算特别有用的一些优点。

  • 多页面支持,除了根目录的root/index.html外,还支持其他根目录以外的页面,比如nest/index.html
  • 对于css预处理器支持更好(这点个人没发现)
  • 支持css代码的code-splitting
  • 优化了异步请求多个chunk文件(不分场景可以同步进行,从而一定程度下减少请求总时间)

6.5 总结

    如果想在生产环境使用unbundle,那么vite是不行的,vite对于线上环境的build,是必须要打包的。vite优化的只是开发环境。而snowpack默认就是unbundle的,因此可以作为前提在生产环境使用unbundle.此外,snowpack的Streaming Imports提供了一套完整的本地map的包管理,不需要将npm包安装到本地,很方便我们在线上和线下使用cdn托管公共库。

@forthealllight forthealllight changed the title js打包时间缩短99%,bundleless生产环境实践总结 js打包时间缩短90%,bundleless生产环境实践总结 Aug 30, 2021
@chipmunktail
Copy link

<div id="container">my name is {{name}}</div>

@yangnaiyue
Copy link

yangnaiyue commented Mar 26, 2022 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants