草庐IT

小程序打包体积过大的解决方案

LittleMoon_lyy 2023-04-07 原文

一 背景

分包是小程序给出的类似于web异步引入的一个方案,把一些初始进入时不需要的页面放到分包里,跳转到对应页面时再去下载分包,从而有效减少主包体积。
项目背景:
公司的小程序项目使用taro来实现一码多端,公共库和基础库放在了主包,导致主包体积体积超出了2M,无法本地预览。本次就是记录一下包体积优化的分析过程和解决思路。

1.目前小程序分包有以下限制:

  • 整个小程序所有分包大小不超过20M
  • 单个分包/主包大小不超过2M

2.什么是主包和分包

小程序在app.json的subpackages字段声明分包结构:
原本的项目路径:

├── app.js
├── app.json
├── app.wxss
├── packageA
│   └── pages
│       ├── cat
│       └── dog
├── packageB
│   └── pages
│       ├── apple
│       └── banana
├── pages
│   ├── index
│   └── logs
└── utils

分包:

{
  "pages":[
    "pages/index",
    "pages/logs"
  ],
  "subpackages": [
    {
      "root": "packageA",
      "pages": [
        "pages/cat",
        "pages/dog"
      ]
    }, {
      "root": "packageB",
      "name": "pack2",
      "pages": [
        "pages/apple",
        "pages/banana"
      ]
    }
  ]
}

说明:

字段类型说明
rootString分包根目录
nameString分包别名,分包预下载时可以使用
pagesStringArray分包页面路径,相对于分包根目录
independentBoolean分包是否是独立分包

使用分包后的打包原则:

  • 声明subpackages后,将按subpackages配置路径进行打包,subpackages配置路径外的目录将被打包到主包中
  • subpackage的根目录不能是另外一个subpackage内的子目录
  • tabBar页面必须在主包内

也就是说,主包用来放启动页/tabBar页面,以及公共资源和js脚本,而分包则根据开发者的配置进行划分。在小程序启动时,默认会下载主包并启动主包内页面,当用户进入分包内某个页面时,客户端会把对应分包下载下来,下载完成后再进行展示。

二 主包的依赖分析

项目打包后,在开发者工具->详情->基本信息->本地代码->代码依赖分析,点击后即可看到主包和各个分包的体积大小。

可以看到主包的体积已经超过2M了,这就必须要对包体积进行优化,否则无法本地预览和发布。
但是我们光看到vendors.js文件体积大是不管用的,我们得知道到底是vendors.js下面的哪些文件占用的体积多,从而才能更好的优化。这就需要借助其他一些工具,如webpack-bundle-analyzer这样的一个webpack插件去做辅助分析,它可以直观的分析出打包的文件包含哪些,大小占比,模块包含关系,依赖项,文件是否重复,压缩后大小如何等等情况。

1.webpack-bundle-analyzer

(1)介绍
本项目的taro版本为3.1.4,taro使用webpack作为内部的打包系统。有时候我们在业务代码中使用了require语法或者import default语法,webpack并不能给我们提供tree-shaking的效果。这时我们需要webpack-bundle-analyzer插件,该插件会在浏览器打开一个可视化的图表页面告诉我们引用各个包的体积。

(2)配置

// 引入依赖
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

const config = {
  ...
  mini: {
    webpackChain (chain, webpack) {
      chain.plugin('analyzer').use(BundleAnalyzerPlugin)
    }
  }
}

编译后我们就可以看到具体的包图了:

开始对上图进行分析:

  • vendors.js里node_modules占比最大,且taro-ui/dist在node_modules占比也很大。这部分需要仔细分析原因。
  • 同时@wallet/taro-cashier占比很大,考虑到这是公司其他团队提供的组件,已通知该团队对包体积进行优化。
  • 剩下的包中我们看到bn.js体积较大,且重复打包。

三 问题解决

关于哪些文件会打包到vendors.js中以及taro项目为什么会出现重复打包的问题
在开发小程序时,taro编译器依赖SplitChunksPlugin插件抽取公共组件,默认主包、分包依赖的公共库都会打包到根目录vendors.js文件中(有一个例外,当只有分包里面有且只有一个页面依赖公共库时,会打包到分包中依赖页面源码中),直接影响到小程序主包大小,很容易超出2M的限制大小。

只要被两个chunk引用的文件,就被打包到主包的common,而分包的每一个页面打包完后都是一个独立的chunk,那就是只要分包里有两个页面引用了同一个文件,这个文件就会被打包到common.js。

1.bn.js重复打包的问题

方法:配置路径别名

module.exports = {
  alias: {
    'bn.js': path.resolve(process.cwd(), 'node_modules', 'bn.js')
  },
}

重新打包,发现bn.js只打包了一次:

2.taro-ui/dist包太大

项目中只有少数几个文件用到了taro-ui的组件,在具体引用时是这样做的:

//引用单个ui组件
import { AtButton } from 'taro-ui'

//全局引入样式(css中)
@import "~taro-ui/dist/style/index.scss";

但是在排查样式时发现全局引用了两次样式:

删除其中一个再次打包:

对比第一次的app.wxss减少了300多k。

而且在打包的时候,taro-ui已经被全部打包进去了,webpack并没有tree-shaking掉未引用的组件,也就是说,官方的按需引入实际上并无法实现。
看了相关的资料以及github上关于这个问题的解决方案:
taro-ui打包问题优化

github关于taro-ui按需引入的解决方案

先采用了最简单的方案,在alias添加:

alias: {
   'taro-ui$': 'taro-ui/lib/index',
},

这样就可以直接加载taro-ui/lib/index中相关的组件,未加载也会被优化掉。

这么做之后直接把taro-ui/dist给干掉了。但是实际上只对js进行按需引入的话是不够的,还想对样式进行按需引入,因为项目的app.wxss和common.wxss体积还是很大,因此考虑第二个方案。

第二个方案,也就是链接一中的方案。

cnpm i babel-plugin-import --save-dev

在babel.config.js中进行如下配置:

const { includes } = require("lodash");
module.exports = {
  plugins: [
    '@babel/plugin-proposal-optional-chaining',
    ["import", {
      libraryName: "taro-ui",
      customName: (name, file) => {
        const nameSection = name.split('-')
        if (nameSection.length === 4) {
          // 子组件的路径跟主组件一样
          nameSection.pop()
        }
        // 指定组件做路径映射
        const pathMap = {
          'tabs/pane': 'tabs-pane',
          'modal-action': 'modal/action',
          'modal-content': 'modal/content',
          'modal-header': 'modal/header'
        }
        const path = nameSection.slice(1).join('-')
        return `taro-ui/lib/components/${pathMap[path] || path}`
      },
      style: (name) => {
        if (includes(name, '/modal')) {
          return 'taro-ui/dist/style/components/modal.scss'
        }

        const wholePath = name.split('/')
        const compName = wholePath[wholePath.length - 1]
        const fix = {
          'tabs-pane': 'tabs',
          // 2、或者在这里写映射,这里正好跟上面的映射相反
          // 'modal/action': 'modal',
          // 'modal/header': 'modal',
          // 'modal/content': 'modal',
        }[compName]
        return `taro-ui/dist/style/components/${fix || compName}.scss`
      }
    }]
  ],
  presets: [
    ['taro', {
      framework: 'react',
      ts: true,
      hot: false // 处理h5 babel运行报错 https://github.com/NervJS/taro/releases?after=v3.1.1
    }]
  ]
}

删除全局引用的css样式:

// @import "~taro-ui/dist/style/index.scss";   

重新打包:

可以看到app.wxss和common.wxss体积都很小了。
注意:
由于taro-ui的路径很不结构化,组件中的子组件可能又要额外的去引用,这样的话代码维护会比较麻烦,因此如果以后要在项目中新增之前没有的taro组件,一定要进行额外的配置。而且如果taro-ui版本升级,那就意味着我们可能需要根据官方的版本不断去优化配置,工作量极大且意义不大。

3.其他的解决方案

(1)使用optimizeMainPackage,terser-webpack-plugin和miniSplitChunksPlugin插件
这部分的优化方案在这里查看:https://taro-docs.jd.com/taro/docs/mini-split-chunks-plugin
具体的配置为:

const TerserPlugin = require("terser-webpack-plugin");
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const MiniSplitChunksPlugin = require('mini-split-chunks-plugin')
const config = {
    mini: { 
        optimizeMainPackage: {
          enable: true
        },
        webpackChain (chain, webpack) {
          process.env.TARO_ENV === 'weapp' && chain.plugin('optimizeMainPackage').use(MiniSplitChunksPlugin).before('miniPlugin')
          chain.plugin('analyzer').use(BundleAnalyzerPlugin);
          chain.merge({
            plugin: {
              terse: {
                plugin: TerserPlugin,
                args: [
                  {
                    minify: TerserPlugin.swcMinify,
                    terserOptions: {
                      compress: true,
                    },
                  }
                ]
              }
            },
          })
        }
       },
    }
}

(2)替换一些体积较大的组件
如将moment.js换成day.js

以上就是小程序包体积优化的整个过程,本次已经将主包的2.54M减少到了1.76M。

有关小程序打包体积过大的解决方案的更多相关文章

  1. ruby - 在 Ruby 程序执行时阻止 Windows 7 PC 进入休眠状态 - 2

    我需要在客户计算机上运行Ruby应用程序。通常需要几天才能完成(复制大备份文件)。问题是如果启用sleep,它会中断应用程序。否则,计算机将持续运行数周,直到我下次访问为止。有什么方法可以防止执行期间休眠并让Windows在执行后休眠吗?欢迎任何疯狂的想法;-) 最佳答案 Here建议使用SetThreadExecutionStateWinAPI函数,使应用程序能够通知系统它正在使用中,从而防止系统在应用程序运行时进入休眠状态或关闭显示。像这样的东西:require'Win32API'ES_AWAYMODE_REQUIRED=0x0

  2. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  3. ruby - 在 Ruby 中编写命令行实用程序 - 2

    我想用ruby​​编写一个小的命令行实用程序并将其作为gem分发。我知道安装后,Guard、Sass和Thor等某些gem可以从命令行自行运行。为了让gem像二进制文件一样可用,我需要在我的gemspec中指定什么。 最佳答案 Gem::Specification.newdo|s|...s.executable='name_of_executable'...endhttp://docs.rubygems.org/read/chapter/20 关于ruby-在Ruby中编写命令行实用程序

  4. ruby-on-rails - Rails 应用程序之间的通信 - 2

    我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

  5. ruby - 无法运行 Rails 2.x 应用程序 - 2

    我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby​​:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r

  6. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  7. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

  8. ruby-on-rails - 如何在我的 Rails 应用程序 View 中打印 ruby​​ 变量的内容? - 2

    我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby​​中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R

  9. ruby - 检查是否通过 require 执行或导入了 Ruby 程序 - 2

    如何检查Ruby文件是否是通过“require”或“load”导入的,而不是简单地从命令行执行的?例如:foo.rb的内容:puts"Hello"bar.rb的内容require'foo'输出:$./foo.rbHello$./bar.rbHello基本上,我想调用bar.rb以不执行puts调用。 最佳答案 将foo.rb改为:if__FILE__==$0puts"Hello"end检查__FILE__-当前ruby​​文件的名称-与$0-正在运行的脚本的名称。 关于ruby-检查是否

  10. ruby-on-rails - 如何在 Gem 中获取 Rails 应用程序的根目录 - 2

    是否可以在应用程序中包含的gem代码中知道应用程序的Rails文件系统根目录?这是gem来源的示例:moduleMyGemdefself.included(base)putsRails.root#returnnilendendActionController::Base.send:include,MyGem谢谢,抱歉我的英语不好 最佳答案 我发现解决类似问题的解决方案是使用railtie初始化程序包含我的模块。所以,在你的/lib/mygem/railtie.rbmoduleMyGemclassRailtie使用此代码,您的模块将在

随机推荐