React-native 包体积优化

3,191 阅读4分钟

开发过 React-native 的同学们应该都知道,客户端运行RN代码,主要是运行RN打包后生成的 bundle 文件,以及一些静态图片资源。本文将从这两点展开优化。

我们先来了解下React-native的打包工具

Metro:

Metro 是官方推荐的 React-native 的打包工具,借助 Metro, 我们能够在编译过程中,通过一些方式来优化编译输出的bundle文件

大概的打包流程如图:

rn.png

图中我们能看到,有三个阶段我们能干预:

1、Resolution 文件解析:从指定入口文件分析依赖,生成一个有依赖关系的图谱

2、Transformation 文件转换:通过babelReact-native代码编译成客户端可以执行的代码片段

3、Serialization 序列化输出:这一阶段主要是根据前两个阶段的执行结果,生成一个或者多个 bundle 文件

Metor 默认会将所有依赖打包成一个bundle,全量内置在客户端。

这样做的好处是:全量内置,页面启动速度快

但是缺点也很明显:前端资源更新,重度依赖客户端,如果线上出现严重bug,只有发布新版本才能解决

以上我们得出结论
1、将一些稳定的、不常更新的第三方包(react, react-native, redux等等)单独打包,内置到客户端
2、将不同的业务分开打包,动态下发不同的业务包,达到热更新的效果

而我们的分包操作主要是依赖 Serialization 提供的两个方法createModuleIdFactory processModuleFilter来实现

公共包实现

我们将公共模块单独打包,将模块打上标签
公共包的createModuleIdFactory代码如下


const commonModulesMap = {};

function createModuleIdFactory() {
  const fileToIdMap = new Map();
  let nextId = 0;
  return path => {
    let id = fileToIdMap.get(path);

    if (typeof id !== "number") {
      id = nextId++;
      fileToIdMap.set(path, id);
      // 记录模块的路径跟Id
      const modulePath = path.relative(process.cwd(), path);
      commonModulesMap[modulePath] = id;
      fs.writeFileSync('./commonModulesMap.json', JSON.stringify(commonModulesMap))
    }

    return id;
  };
}

我们新建common.js,引入第三方依赖。

import "react";
import "react-native";

新建metro.base.config.js,引入createModuleIdFactory

module.exports = {
  serializer: {
    createModuleIdFactory
  }
};

新建bundles目录

mkdir bundles

执行打包命令

react-native bundle --platform android --dev false --entry-file  common.js --bundle-output bundles/common.android.bundle --assets-dest bundles --config ./metro.base.config.js --reset-cache

打包后我们看到commonModulesMap.json文件,每个依赖都打上了数字标签。

{
  "common-entry.js": 1,
  "node_modules/react/index.js": 2,
  "node_modules/react/cjs/react.production.min.js": 3,
  "node_modules/object-assign/index.js": 4,
  "node_modules/@babel/runtime/helpers/extends.js": 5,
  "node_modules/react-native/index.js": 6
}

业务包实现

业务包如下,公共模块复用公共包当中的Id, 业务包随机数Id自增

const commonModulesMap = require('./commonModulesMap.json');
function createModuleIdFactory() {
  let nextId =  +`${+new Date()}`.slice(-9);
  const fileToIdMap = new Map();

  return (path) => {
    // 公共包的Id
    const modulePath = path.relative(process.cwd(), path);
    if (commonModulesMap[modulePath]) {
      return commonModulesMap[modulePath];
    }
    // 业务包的Id
    let id = fileToIdMap.get(path);
    if (typeof id !== 'number') {
      id = nextId + 1;
      nextId = nextId + 1;
      fileToIdMap.set(path, id);
    }
    return id;
  };
}

打业务包的时候,如果模块在公共包已经存在,就返回false,不将其打包在内。
processModuleFilter代码如下

function processModuleFilter() {
  return (module) => {
    const { path } = module;
    const modulePath = path.relative(process.cwd(), path);
    if (
      path.indexOf('__prelude__') !== -1 ||
      path.indexOf('/node_modules/react-native/Libraries/polyfills') !== -1 ||
      path.indexOf('source-map') !== -1 ||
      path.indexOf('/node_modules/metro-runtime/src/polyfills/require.js') !== -1
    ) {
      return false;
    }
    if (commonModulesMap[modulePath]) {
      return false;
    }
    return true;
  };
}

新建metro.business.config.js,引入createModuleIdFactory以及processModuleFilter

module.exports = {
  serializer: {
    createModuleIdFactory,
    processModuleFilter
  }
};

编译业务包,这里以一个业务包为例,多个业务包循环编译即可

react-native bundle --platform android --dev false --entry-file index.js --bundle-output bundles/business.android.bundle --assets-dest bundles --config ./metro.business.config.js  --reset-cache

通过以上策略,业务包代码压缩后的体积大幅减少了,如何更细粒度减少下发包体积呢

如何减小下发包体积

补丁式更新 diff_match_patch

1、diff 补丁

const diffMatchPatch = require('diff-match-patch')
const dmp = new diffMatchPatch();

const pre = "线上运行的资源包"
const next = "新资源包"

const diff = dmp.diff_main(pre, next)
const patches = dmp.patch_make(diff)

patches 就是补丁内容,压缩之后只有几kb大小

2、客户端下载补丁并更新

const diffMatchPatch = require('diff-match-patch')
const dmp = new diffMatchPatch()

const pre = "线上运行的资源包"
const patches = "下发的补丁文件"

const result = dmp.patch_apply(patches, pre)

result就是我们打好补丁的最新资源包。

静态图片上CDN

默认做法是静态图片全量内置在客户端

优点:加载速度快
缺点:下发包体积增大 我们可以选择将图片上传到CDN,在减少下发包体积的同时,CDN也帮助我们更快的加载图片资源

有两种方式可以做
1、React-native Image 组件提供了动态更改图片路径的方法 setCustomSourceTransformer
我们在访问时动态改变图片路径,达到访问CDN的目的

Image.resolveAssetSource.setCustomSourceTransformer((resolver) => {
  // CDN 地址
  const cdnPath = 'https://******';
  resolver.jsbundleUrl = cdnPath;
  
  let resolveAsset;
  if (Platform.OS === "android") { 
    resolveAsset = resolver.drawableFolderInBundle(); 
  } else { 
    resolveAsset = resolver.scaledAssetURLNearBundle(); 
  }

  return resolveAsset;
});

2、将本地地址替换为远程地址
React-native 加载图片的两种方式:

<Image source="require(本地图片地址)" />  
<Image source="{uri: cdn地址}" />

我们只需要在打包前将图片上传CDN,生成本地路径对应的CDN地址的Map文件,动态的将 require(本地图片地址) 替换为 {uri: cdn地址} 即可

总结

1、我们通过将第三方包抽离成单独的公共包,又将不同的业务拆分成独立的业务包
2、为了更细粒度的实现热更新,我们又增加了打补丁的方式去更新业务包
3、通过图片上CDN,减小了下发包体积