umi 性能优化:使用CDN一篇就够

4,350 阅读7分钟

前言

在使用umi的过程中,随着项目的日渐膨胀,由于在umi3之前打包的时候都是集中在一个大文件上的,这就是导致一个文件会很大,有幸见过一个js文件超过了10M,而且项目所运行的网络又是3G网速。这时候就会出现以下这种场景:

用户:“你们的网站打开好慢啊,要一分多钟,这个要优化一下”

产品:“这个提个需求给前端吧”

前端:“那就拆包吧”

于是就吧啦吧啦一顿代码输出,最终拆包出来了。随后出现的的文件就有umi.js、echarts.js、dhtmlx-gantt.js等。

但是今天的主题不是讲拆包,而将拆另有一个优化方式:CDN

CDN是什么

CDN的全称是 Content Delivery Network,即内容分发网络。CDN 是构建在网络之上的内容分发网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN 的关键技术主要有内容存储和分发技术。—— 科学百科

16c02ed80b0a1aa8.png

简单理解一下上面的图:

  1. 当用户动态请求服务器时,会动态的返回结果。
  2. 服务器会先提前PUSH静态资源到OSS上,保持数据同步。
  3. 当用户请求静态资源时,CDN会根据就近原则获取到所需的内容,如果CDN中没有缓存到,那么就会向OSS服务器PULL,最后返回请求的静态资源到客户端上。

CDN都这么好用了,为什么动态请求数据不也放在CDN上?

动态内容也是可以的,但是比较难,毕竟动态的内容是根据每一个用户来改变的或者根据每个时间段来改变的,服务器很难做到提前测试到每个用户的动态内容,然后提前给到CDN。如果等用户索取动态内容,CDN再问源服务器索取,这样CDN提供不了多少的加速服务,也就存在的意义。但确实CDN可以提供动态内容服务,比方说时间,时间是一直变动的,如果让源服务器一直提供时间动态内容,万一网络不稳定,时间就没办法同步,因此有些CDN会提供可以在CDN上的接口,让源服务器用这些接口,而不是源服务器自己的代码,这样用户就可以直接CDN上获取时间,而不是从源服务器上获取了,当然动态内容还有很多的解决办法,也存在很多问题。

国内免费的公用静态资源CDN

cdnjs

cdnjs 是一项免费的开源 CDN 服务,受到超过 12.5% 的网站的信任,每月处理超过 2000 亿个请求,由 Cloudflare 提供支持。

jsdelivr

jsdeliver是一个免费的开源文件CDN(内容分发网络)。我们与Github和NPM紧密集成,使我们能够自动为几乎每个开源项目提供可靠的CDN服务。

提供了一个稳定的CDN,可以在生产中使用的热门网站与大量的流量。没有带宽限制或高级功能,任何人都可以完全免费使用。

UNPKG

unpkg CDN由Cloudflare提供支持,Cloudflare是世界上最大和最快的云网络平台之一。

静态资源CDN网站有了,下面就让我们熟悉一下webpack的externals

externals

externals 配置选项提供了「从输出的 bundle 中排除依赖」的方法。相反,所创建的 bundle 依赖于那些存在于用户环境(consumer's environment)中的依赖。此功能通常对 library 开发人员来说是最有用的,然而也会有各种各样的应用程序用到它。—— webpack官网

通过webpack的例子,来认识一下externals的使用

防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖(external dependencies)

例如,从 CDN 引入 jQuery,而不是把它打包:

index.html

<script
  src="https://code.jquery.com/jquery-3.1.0.js"
  integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
  crossorigin="anonymous"
></script>

webpack.config.js

module.exports = {
  //...
  externals: {
    jquery: 'jQuery',
  },
};

这样就剥离了那些不需要改动的依赖模块,换句话,下面展示的代码还可以正常运行:

import $ from 'jquery';

$('.my-element').animate(/* ... */);

上面 webpack.config.js 中 externals 下指定的属性名称 jquery 表示 import $ from 'jquery' 中的模块 jquery 应该从打包产物中排除。 为了替换这个模块,jQuery 值将用于检索全局 jQuery 变量,因为默认的外部库类型是 var,请参阅 externalsType

上面我们认识了webpack的配置中如何去配置externals,但是项目使用的是umi。umi里边也提供externals的写法,但是有点不一样。

umi的externals

设置哪些模块可以不被打包,通过 <script> 或其他方式引入,通常需要和 scripts 或 headScripts 配置同时使用。

export default {
  externals: {
    react: 'window.React',
  },
  scripts: ['https://unpkg.com/react@18.2.0/umd/react.production.min.js'],
};

细心就会发现,这样只能传入一个URL字符串,Script的其他值怎么传呢?

可以使用umi的插件API的addHTMLHeadScripts来实现

里边还有很多的拓展功能,具体的参考umi官网

对比打包前后的区别

接下来,我们分别根据未优化和已优化打包的方式来对一下。

import styles from './index.less';
import { Button, Space, Checkbox, Form, Input } from 'antd';
import { ECharts } from 'echarts';
import { useEffect, useRef, useState } from 'react';

import * as echarts from 'echarts';

interface AutoChartProps {
  option: echarts.EChartsOption;
}

const AutoChart: React.FC<AutoChartProps> = (props) => {
  const chartRef = useRef(null);

  const [chart, setChart] = useState<echarts.ECharts>();

  const handleResize = () => chart?.resize();

  const init = () => {
    if (chart) {
      // 建议替换为 ResizeObserver (2023.5.25)
      window.removeEventListener('resize', handleResize);
    }

    const _chart = echarts.init(chartRef.current as any as HTMLElement);
    _chart.setOption(props.option);
    window.addEventListener('resize', handleResize);
    setChart(_chart);
  };

  useEffect(() => {
    init();
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, [props.option]);

  return <div ref={chartRef} style={{ height: '100%', width: '100%' }} />;
};

const onFinish = (values: any) => {
  console.log('Success:', values);
};

const onFinishFailed = (errorInfo: any) => {
  console.log('Failed:', errorInfo);
};

type FieldType = {
  username?: string;
  password?: string;
  remember?: string;
};

var option = {
  xAxis: {
    type: 'category',
    data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
  },
  yAxis: {
    type: 'value',
  },
  series: [
    {
      data: [150, 230, 224, 218, 135, 147, 260],
      type: 'line',
    },
  ],
};
export default function IndexPage() {
  return (
    <div>
      <div style={{ width: '400px', height: '400px' }}>
        <AutoChart option={option} />
      </div>
      <Space wrap>
        <Button type="primary">Primary Button</Button>
        <Button>Default Button</Button>
        <Button type="dashed">Dashed Button</Button>
        <Button type="text">Text Button</Button>
        <Button type="link">Link Button</Button>
      </Space>

      <Form
        name="basic"
        labelCol={{ span: 8 }}
        wrapperCol={{ span: 16 }}
        style={{ maxWidth: 600 }}
        initialValues={{ remember: true }}
        onFinish={onFinish}
        onFinishFailed={onFinishFailed}
        autoComplete="off"
      >
        <Form.Item<FieldType>
          label="Username"
          name="username"
          rules={[{ required: true, message: 'Please input your username!' }]}
        >
          <Input />
        </Form.Item>

        <Form.Item<FieldType>
          label="Password"
          name="password"
          rules={[{ required: true, message: 'Please input your password!' }]}
        >
          <Input.Password />
        </Form.Item>

        <Form.Item<FieldType>
          name="remember"
          valuePropName="checked"
          wrapperCol={{ offset: 8, span: 16 }}
        >
          <Checkbox>Remember me</Checkbox>
        </Form.Item>

        <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
          <Button type="primary" htmlType="submit">
            Submit
          </Button>
        </Form.Item>
      </Form>
      <h1 className={styles.title}>Page index</h1>
    </div>
  );
}

未优化

注释部分是拆包内容

import { defineConfig } from 'umi';

export default defineConfig({
  nodeModulesTransform: {
    type: 'none',
  },
  routes: [
    { path: '/', component: '@/pages/index' },
  ],
  analyze: {
    analyzerMode: "server",
    analyzerPort: 8889,
    openAnalyzer: true,
    // generate stats file while ANALYZE_DUMP exist
    generateStatsFile: false,
    statsFilename: "stats.json",
    logLevel: "info",
    defaultSizes: "stat", // stat  // gzip  // parsed
  },
  // chunks: ['echarts','antd','vendors',  'umi'],
  chainWebpack(config, args) {
    // if (process.env.ANALYZE) {
    //   config
    //     .plugin("webpack-bundle-analyzer")
    //     .use(require("webpack-bundle-analyzer").BundleAnalyzerPlugin);
    // }
    // webpack 拆包
    // config.merge({
    //   optimization: {
    //     splitChunks: {
    //       chunks: 'all', //async异步代码分割 initial同步代码分割 all同步异步分割都开启
    //       automaticNameDelimiter: '.',
    //       name: true,
    //       minSize: 30000, // 引入的文件大于30kb才进行分割
    //       //maxSize: 50000, // 50kb,尝试将大于50kb的文件拆分成n个50kb的文件
    //       minChunks: 1, // 模块至少使用次数
    //       // maxAsyncRequests: 5,    // 同时加载的模块数量最多是5个,只分割出同时引入的前5个文件
    //       // maxInitialRequests: 3,  // 首页加载的时候引入的文件最多3个
    //       // name: true,             // 缓存组
    //       cacheGroups: {
    //         echarts: {
    //           name: 'echarts',
    //           test: /[\\/]node_modules[\\/](echarts)[\\/]/,
    //           priority: -9,
    //           enforce: true,
    //         },
    //         antd: {
    //           name: 'antd',
    //           test: /[\\/]node_modules[\\/](@ant-design|antd|antd-mobile)[\\/]/,
    //           priority: -10,
    //           enforce: true,
    //         },
    //         vendors: {
    //           name: 'vendors',
    //           test: /[\\/]node_modules[\\/]/,
    //           priority: -11,
    //           enforce: true,
    //         },
    //       },
    //     },
    //   },
    // });
    return config;
  },
  fastRefresh: {},
});

执行 yarn analyze

image.png

公共也就5.25M,可以看到echarts在umi.js中,而且占的位置是非常的大的。

优化

import { defineConfig } from 'umi';

export default defineConfig({
  nodeModulesTransform: {
    type: 'none',
  },
  routes: [
    { path: '/', component: '@/pages/index' },
  ],
  externals: {
    react: "React",
    "react-dom": "ReactDOM",
    // axios: "axios",
    // moment: "moment",
    dayjs: "dayjs",
    antd:"antd",
    echarts:'echarts'
  },
  links: [{ href: 'https://unpkg.com/antd@5.8.3/dist/reset.css', rel: 'stylesheet' }],
  headScripts: [
    "https://unpkg.com/react@18.2.0/umd/react.production.min.js",
    "https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js",
    // "https://unpkg.com/axios@1.4.0/dist/axios.min.js",
    // "https://unpkg.com/moment@2.29.4/min/moment.min.js",
    "https://unpkg.com/dayjs/dayjs.min.js",
    "https://unpkg.com/antd@5.8.3/dist/antd.min.js",
    "https://unpkg.com/echarts@5.4.3/dist/echarts.min.js"
  ],
  analyze: {
    analyzerMode: 'server',
    analyzerPort: 8889,
    openAnalyzer: true,
    // generate stats file while ANALYZE_DUMP exist
    generateStatsFile: false,
    statsFilename: 'stats.json',
    logLevel: 'info',
    defaultSizes: 'gzip', // stat  // gzip  // parsed
  },
  fastRefresh: {},
});

执行 yarn analyze

image.png

可以看到也就1.54M,奇怪这个echarts和antd去哪里了?

其实在这里

image.png

image.png

最后

今天一不小心又卷了一下,欢迎大家来分享umi的性能优化方式。

由于项目使用的是umi3.x,上面的案例也是通过umi3.x创建的。umi4.x也是同样的道理,只不过拆包方式有所不同。

参考文章

什么是CDN?CDN能为我们做什么?我们为什么要了解他?

【前端词典】CDN 带来这些性能优化