如何分析和改进“Create React App”生产构建

2025-06-04

如何分析和改进“Create React App”生产构建

如果您对本教程的视频版本感兴趣,请查看下方链接。您可以参考本博客中的代码进行学习。(视频完全可选,博客文章中涵盖了每个步骤和说明。)

创建 React App 分析教程

在本教程中,我们将逐步了解如何使用非常容易设置的自定义 webpack 配置来分析和优化您的Create React App项目。

我们将使用进行小调整的示例,并尝试导入模块和拆分代码的不同方式,以查看它对捆绑包大小和性能的直接影响。

这里的目标是通过查看当您对应用程序进行微小更改时生产版本发生的确切变化来帮助您更好地理解webpack实际在做什么。

首先,我们将创建一个名为something-big-and-bloated



npx create-react-app something-big-and-bloated --template typescript


Enter fullscreen mode Exit fullscreen mode

接下来我们将安装分析项目所需的依赖项。



npm install @craco/craco webpack-bundle-analyzer --save-dev


Enter fullscreen mode Exit fullscreen mode
  • craco :使用自定义 webpack 配置与Create React App 的工具
  • webpack-bundle-analyzer:用于分析包大小的 webpack 插件

我们需要craco在项目根目录中创建一个配置文件来包含我们的 webpack 插件:

craco.config.js



const BundleAnalyzerPlugin =
  require("webpack-bundle-analyzer").BundleAnalyzerPlugin;

module.exports = function () {
  return {
    webpack: {
      plugins: [new BundleAnalyzerPlugin({ analyzerMode: "server" })],
    },
  };
};


Enter fullscreen mode Exit fullscreen mode

如果我们运行生产版本的常用npm run build脚本,它将使用标准react-scripts方法。

但是,如果我们运行craco build它,它仍然会运行相同的进程,但会注入你在craco.config.js文件中包含的任何 webpack 配置。真是太棒了。

让我们尝试一下。我们将脚本中创建一个名为analyze 的新条目:package.json



{
  ...
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "analyze": "craco build"
  }
}


Enter fullscreen mode Exit fullscreen mode

现在运行:



npm run analyze


Enter fullscreen mode Exit fullscreen mode

因为我们在 craco 配置中设置了analyzerMode"server"所以我们将自动打开浏览器并将结果作为网页提供(如果您希望在不涉及浏览器的情况下获得输出,则可以使用该选项)"json"

初始大小

您可以将鼠标悬停在区块中的每个模块上,也可以使用左上角的小箭头弹出一个抽屉。您将获得三个不同的值:

  • stat:原始源代码的大小
  • 解析:与编译包相同的代码大小
  • gzipped:经过 gzip 压缩后编译包的大小

因此,对于大多数部署,您可以将 gzip 压缩后的值视为所需的网络使用量,将解析后的大小视为解压完成后浏览器将处理的代码量。请注意,对于 CPU 性能较弱的用户来说,此值可能带来问题,就像 gzip 压缩后的大小对于网络速度较慢的用户一样。两者都需要考虑。

在本教程的 v4.0.3 版本中,create-react-app我得到的 stat / parsed / gzip 大小分别为 205kb / 135kb / 44kb。您可以看到,开箱即用会产生一些开销(不过,对于大多数用户来说,为了方便起见,这只是一个小小的代价)。

现在我们尝试添加一些库,看看这个值是如何变化的。我们会思考导入的方式,看看如何通过只导入我们需要的库来更好地控制包的大小。

我将选择一个相当流行的 UI 库,名为 MUI(Material UI)。它是一个很好的例子,说明如果打包不当,大型软件包会严重拖累你的应用。它将是我们教程的一个很好的例子。

我们需要以下软件包:



npm install @mui/material @mui/icons-material @emotion/react @emotion/styled --save


Enter fullscreen mode Exit fullscreen mode

在开始之前,我们先再次运行一下分析器。记住,我们已经添加了这些库,但实际上还没有使用它们。你觉得我们的包大小会增加吗?我们来看一下:



npm run analyze


Enter fullscreen mode Exit fullscreen mode

又是 205kb / 135kb / 44kb。结果完全一样。太棒了!这意味着 webpack 没有包含我们实际上不使用的库。它做得很好。

现在我们要从 MUI 导入一个组件。我们会选择一个相对复杂的组件,不仅仅是一个按钮。让我们使用快速拨号!在 中创建一个新的组件文件src

src/CustomSpeedDial.tsx



import React from "react";
import Box from "@mui/material/Box";
import SpeedDial from "@mui/material/SpeedDial";
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
import SpeedDialAction from "@mui/material/SpeedDialAction";
import FileCopyIcon from "@mui/icons-material/FileCopyOutlined";
import SaveIcon from "@mui/icons-material/Save";
import PrintIcon from "@mui/icons-material/Print";
import ShareIcon from "@mui/icons-material/Share";

const actions = [
  { icon: <FileCopyIcon />, name: "Copy" },
  { icon: <SaveIcon />, name: "Save" },
  { icon: <PrintIcon />, name: "Print" },
  { icon: <ShareIcon />, name: "Share" },
];

export default function CustomSpeedDial() {
  return (
    <Box sx={{ height: 320, transform: "translateZ(0px)", flexGrow: 1 }}>
      <SpeedDial
        ariaLabel="SpeedDial basic example"
        sx={{ position: "absolute", bottom: 16, left: 16 }}
        icon={<SpeedDialIcon />}
      >
        {actions.map((action) => (
          <SpeedDialAction
            key={action.name}
            icon={action.icon}
            tooltipTitle={action.name}
          />
        ))}
    </Box>
  );
}


Enter fullscreen mode Exit fullscreen mode

将文件内容替换App.tsx为以下内容:

src/App.tsx



import React from "react";
import CustomSpeedDial from "./CustomSpeedDial";

function App() {
  return <CustomSpeedDial />;
}

export default App;


Enter fullscreen mode Exit fullscreen mode

运行开发服务器来检查:



npm run start


Enter fullscreen mode Exit fullscreen mode

MUI 快速拨号

一切看起来都很好。让我们看看这对我们的构建有多大影响。再次运行我们的分析命令:



npm run analyze


Enter fullscreen mode Exit fullscreen mode

与快速拨号捆绑

我们的 bundle 大小现在高达 660kb / 270kb / 88kb。对于一个组件来说,这可是个显著的增长!当然,请记住,它相当复杂,一旦使用 MUI,就需要包含所有其他使 MUI 正常运行的依赖项。

我敢打赌,如果你再添加一个组件,你就不会看到这么大的飞跃。事实上,我们现在就可以试试。在你的 SpeedDial 组件中添加以下内容:

src/CustomSpeedDial.tsx



import React from "react";
import Box from "@mui/material/Box";
import SpeedDial from "@mui/material/SpeedDial";
import SpeedDialIcon from "@mui/material/SpeedDialIcon";
import SpeedDialAction from "@mui/material/SpeedDialAction";
import FileCopyIcon from "@mui/icons-material/FileCopyOutlined";
import SaveIcon from "@mui/icons-material/Save";
import PrintIcon from "@mui/icons-material/Print";
import ShareIcon from "@mui/icons-material/Share";

// NEW
import Button from "@mui/material/Button";

const actions = [
  { icon: <FileCopyIcon />, name: "Copy" },
  { icon: <SaveIcon />, name: "Save" },
  { icon: <PrintIcon />, name: "Print" },
  { icon: <ShareIcon />, name: "Share" },
];

export default function CustomSpeedDial() {
  return (
    <Box sx={{ height: 320, transform: "translateZ(0px)", flexGrow: 1 }}>
      {/* NEW */}
      <Button variant="contained">Hello world!</Button>
      <SpeedDial
        ariaLabel="SpeedDial basic example"
        sx={{ position: "absolute", bottom: 16, left: 16 }}
        icon={<SpeedDialIcon />}
      >
        {actions.map((action) => (
          <SpeedDialAction
            key={action.name}
            icon={action.icon}
            tooltipTitle={action.name}
          />
        ))}
      </SpeedDial>
    </Box>
  );
}


Enter fullscreen mode Exit fullscreen mode

我们导入了上面的按钮并将其包含在我们的快速拨号中(两行新行标有“NEW”注释。)

再次运行时,npm run analyze我们得到的结果……几乎一样!677kb / 278kb / 89kb。我们可以看到,按钮相对于文件包的大小来说非常小,因为它的大部分构建块已经包含在快速拨号中了。

但是现在让我们比较一下使用传统的 commonJS 导入的情况。

将以下行添加到CustomSpeedDial组件的最顶部(如果 ESLint 抱怨导入顺序,请将该行放在所有导入语句之后)

src/CustomSpeedDial.tsx



const material = require("@mui/material");


Enter fullscreen mode Exit fullscreen mode

再分析一下:



npm run analyze


Enter fullscreen mode Exit fullscreen mode

MUI 一切捆绑

我的天哪!1.97* MB * / 697kb / 194kb。

发生了什么?看起来我们把整个MUI 库都打包了。Popper?Tooltip.js?我们都没用到,但它们在我们的代码块里占用了好多空间。

事实证明,当我们使用 ES6 模块时,webpack 非常擅长根据我们导入和导出的内容来确定我们实际正在使用哪些代码。

这个过程叫做tree shake,它需要你使用 ES6 模块才能工作。你可以看到,这样做会对最终的打包结果产生非常显著的影响。

我们当前的程序在功能上与之前的程序完全相同,但由于导入了一次 commonJS,它的大小竟然是之前的 3 倍。哎呀!

不过,我们要做的就是这样做。我们不会删除它require,而是CustomSpeedDial保留它,并引入一个叫做代码拆分的功能,作为另一个可用的选项。

当你的应用程序中存在某个组件、页面或通用部分并非每个访问者都需要时,代码拆分会非常有效。它可能是一个仅在用户预订时显示的日期选择器,也可能是一个只有一小部分用户需要的“帮助”页面。

我们可以使用 React 的惰性和悬念功能将这些部分分解为单独的捆绑块,并且​​仅在必要时加载它们。

让我们更新一下App.tsx。这里有很多内容需要解释,所以我们先展示代码,然后再进行分解:

src/App.tsx



import CircularProgress from "@mui/material/CircularProgress";
import Button from "@mui/material/Button";
import React, { Suspense, useState } from "react";

// 1
const CustomSpeedDial = React.lazy(() => import("./CustomSpeedDial"));

function App() {
  // 2
  const [showSpeedDial, setShowSpeedDial] = useState(false);

  // 4
  if (showSpeedDial) {
    return (
      // 5
      <Suspense fallback={<CircularProgress />}>
        <CustomSpeedDial />
      </Suspense>
    );
  }

  return (
    // 3
    <Button variant="contained" onClick={() => setShowSpeedDial(true)}>
      Click to load speed dial
    </Button>
  );
}

export default App;


Enter fullscreen mode Exit fullscreen mode

这些数字的顺序有点乱,但这是故意的。你会看到它遵循了组件的实际流程。

  1. 我们“延迟”导入了该CustomSpeedDial模块。记住,它是用于require整个 MUI 包的模块,大小约为 1-2MB。使用延迟导入后,只有当我们的主要组件 (CustomSpeedDial) 实际尝试渲染它时,才会进行导入。我们会看到,默认情况下不会进行渲染。
  2. 一个 React 状态布尔值,用于跟踪我们是否要渲染哪个组件。默认false值表示我们不会渲染CustomSpeedDial
  3. 我们的默认组件是Button直接从 MUI 导入的 basic 组件。按下此按钮时,它会将 的值设置showSpeedDialtrue
  4. 一旦showSpeedDialtrue,下次渲染时就会执行此分支。之所以重新渲染,是因为我们更新了一个有状态的 React 值 (showSpeedDial)。
  5. 该组件的作用Suspense是告诉 React 在等待模块导入时要渲染什么。根据模块大小,可能需要一秒钟或更长时间。在我们的示例中,我们使用 MUICircularProgress来暗示模块加载时的加载状态。加载完成后,它会切换到渲染 Suspense 组件的子项。

现在是时候尝试一下了!我们将从分析开始:



npm run analyze


Enter fullscreen mode Exit fullscreen mode

MUI 代码拆分

.js这真的很有趣。Webpack 创建了新的独立块。当你切换左侧的抽屉时,你会注意到有更多块。

事实上,左边最大的这个块3.5d1a4e88.chunk.js(1.52mb / 475kb / 122kb)在我们应用的默认加载过程中甚至都没有被使用。根据我们之前的了解,我们可以看出,这个巨大的块肯定是我们的CustomSpeedDial.tsx组件,它通过 commonJS import 导入了所有的 MUI 组件require

右边是更小的 bundle 2.c5828938.chunk.js,包含Button和 之类的内容ButtonBase。这是每次页面加载时都会加载的块。我们可以看一下它的大小 (451kb / 214kb / 69kb),稍后再验证。

由于我们的最终目标是确保生产环境的应用尽可能高效地运行,因此我们希望在生产环境的应用上运行测试。使用以下命令构建生产环境的应用:



bpm run build


Enter fullscreen mode Exit fullscreen mode

接下来,我们需要为build创建的目录提供服务。如果您有自己喜欢的本地服务器,那就使用它吧!如果没有,只需添加serve包:



npm install serve --save-dev


Enter fullscreen mode Exit fullscreen mode

然后使用它来提供build目录:



npx serve build


Enter fullscreen mode Exit fullscreen mode

您可以在http://localhost:3000/ (或服务器在命令行上指定的任何端口)找到该页面

按 F12 打开浏览器的开发者工具,然后点击“网络”选项卡。此过程在 Chrome、Edge 和 Firefox 中大致相同。

我使用的是 Firefox,所以截图应该和你的体验一致。如果你使用的是其他浏览器,选项仍然会保留,只是位置可能不同。

勾选“禁用缓存”复选框,这样每次刷新时都会加载 JS 文件,而不是浏览器的缓存版本。我们希望能够查看加载时间和大小。

现在点击刷新按钮(F5)。

代码拆分首次加载

正如我们预测的那样,我们的总传输量为 82KB,其中 69KB 是c5828938我们确定的突出显示的较小块(请记住,这是一个服务生产版本,因此我们正在使用 GZIP 大小,就像您的真实应用程序为真实用户所做的那样)

找不到那个 122KB 压缩包。我们点击应用程序上的“加载快速拨号”按钮。

代码拆分第二次加载

有一个 122KB 的块,CustomSpeedDial里面有我们的组件。

它只按需加载代码,这有多酷?

总结

我希望您可以开始集思广益,寻找减少应用程序捆绑包大小的方法,并可能引入代码拆分来改善初始加载时间。

另外值得注意的是:这些技巧并非Create React App独有。我们只是引入了一个名为 的特殊工具,craco用于配置 webpack。任何运行 webpack 的应用程序都有可能从这些技巧中受益!

如果我没提的话,那真是太不负责任了,Create React App确实推荐了一款类似的工具,它不需要用户手动操作craco (虽然我个人觉得读取数据的方式不太直观),但仍然可以很好地完成工作。点击此处了解详情。

继续学习

请查看我的其他学习教程。如果您觉得其中有任何内容有用,请随时发表评论或提出问题并与他人分享:


想要了解更多类似教程,请在 Twitter 上关注我@eagleson_alex

文章来源:https://dev.to/alexeagleson/how-to-analyze-and-improve-your-create-react-app-production-build-4f34
PREV
我的编程之旅:保持耐心并避免倦怠。
NEXT
为了让金融机构崩溃,我们将与开发人员合作!