From 1b564052994d6aefbb2c091d7dbff6f1e11cebbb Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?=
 <sxzz@sxzz.moe>
Date: Mon, 21 Aug 2023 11:33:51 +0800
Subject: [PATCH] ci: improved size report (#8992)

---
 .eslintrc.cjs                      |   7 +-
 .github/contributing.md            |   2 -
 .github/workflows/ci.yml           |  20 ----
 .github/workflows/size-report.yml  |  61 +++++++++++
 package.json                       |  12 ++-
 packages/size-check/README.md      |   3 -
 packages/size-check/brotli.js      |   6 --
 packages/size-check/package.json   |  11 --
 packages/size-check/src/index.ts   |   6 --
 packages/size-check/vite.config.js |  15 ---
 pnpm-lock.yaml                     | 162 ++++++++++++++++++++++++-----
 scripts/aliases.js                 |   7 +-
 scripts/build.js                   |  50 ++++++---
 scripts/size-report.ts             | 105 +++++++++++++++++++
 scripts/usage-size.ts              |  99 ++++++++++++++++++
 tsconfig.build.json                |   1 -
 16 files changed, 443 insertions(+), 124 deletions(-)
 create mode 100644 .github/workflows/size-report.yml
 delete mode 100644 packages/size-check/README.md
 delete mode 100644 packages/size-check/brotli.js
 delete mode 100644 packages/size-check/package.json
 delete mode 100644 packages/size-check/src/index.ts
 delete mode 100644 packages/size-check/vite.config.js
 create mode 100644 scripts/size-report.ts
 create mode 100644 scripts/usage-size.ts

diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index ec05a113113..04ecf049ca9 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -74,12 +74,7 @@ module.exports = {
     },
     // Node scripts
     {
-      files: [
-        'scripts/**',
-        '*.{js,ts}',
-        'packages/**/index.js',
-        'packages/size-check/**'
-      ],
+      files: ['scripts/**', '*.{js,ts}', 'packages/**/index.js'],
       rules: {
         'no-restricted-globals': 'off',
         'no-restricted-syntax': 'off'
diff --git a/.github/contributing.md b/.github/contributing.md
index e728e4cc8eb..0c6771ca0b4 100644
--- a/.github/contributing.md
+++ b/.github/contributing.md
@@ -248,8 +248,6 @@ This repository employs a [monorepo](https://en.wikipedia.org/wiki/Monorepo) set
 
   - `template-explorer`: A development tool for debugging compiler output, continuously deployed at https://template-explorer.vuejs.org/. To run it locally, run [`nr dev-compiler`](#nr-dev-compiler).
 
-  - `size-check`: Used for checking built bundle sizes on CI.
-
 ### Importing Packages
 
 The packages can import each other directly using their package names. Note that when importing a package, the name listed in its `package.json` should be used. Most of the time the `@vue/` prefix is needed:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 232c69b3b75..8c08c9a935a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -114,23 +114,3 @@ jobs:
 
       - name: Run type declaration tests
         run: pnpm run test-dts
-
-  size:
-    runs-on: ubuntu-latest
-    if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
-    env:
-      CI_JOB_NUMBER: 1
-    steps:
-      - uses: actions/checkout@v3
-
-      - name: Install pnpm
-        uses: pnpm/action-setup@v2
-
-      - name: Set node version to 18
-        uses: actions/setup-node@v3
-        with:
-          node-version: 18
-          cache: 'pnpm'
-
-      - run: PUPPETEER_SKIP_DOWNLOAD=1 pnpm install
-      - run: pnpm run size
diff --git a/.github/workflows/size-report.yml b/.github/workflows/size-report.yml
new file mode 100644
index 00000000000..87c6865927b
--- /dev/null
+++ b/.github/workflows/size-report.yml
@@ -0,0 +1,61 @@
+name: size report
+
+on:
+  pull_request:
+    branches:
+      - main
+
+permissions:
+  contents: read
+  pull-requests: write
+
+jobs:
+  size:
+    runs-on: ubuntu-latest
+
+    steps:
+      - uses: actions/checkout@v3
+
+      - name: Install pnpm
+        uses: pnpm/action-setup@v2
+
+      - name: Set node version to LTS
+        uses: actions/setup-node@v3
+        with:
+          node-version: lts/*
+          cache: pnpm
+
+      - run: PUPPETEER_SKIP_DOWNLOAD=1 pnpm install
+      - run: pnpm run size
+
+      - name: Download Previous Size Report
+        id: download-artifact
+        uses: dawidd6/action-download-artifact@v2
+        with:
+          branch: main
+          name: size-report
+          path: temp/size-prev
+          if_no_artifact_found: warn
+
+      - name: Upload Size Report
+        uses: actions/upload-artifact@v3
+        with:
+          name: size-report
+          path: temp/size
+
+      - name: Compare size
+        run: pnpm tsx scripts/size-report.ts > size.md
+
+      - name: Read Size Markdown
+        id: size-markdown
+        uses: juliangruber/read-file-action@v1
+        with:
+          path: ./size.md
+
+      - name: Create Comment
+        uses: actions-cool/maintain-one-comment@v3
+        with:
+          body: |
+            ${{steps.size-markdown.outputs.content}}
+            <!-- VUE_CORE_SIZE -->
+          body-include: '<!-- VUE_CORE_SIZE -->'
diff --git a/package.json b/package.json
index d514ccd9190..b4ee1b00082 100644
--- a/package.json
+++ b/package.json
@@ -7,9 +7,10 @@
     "dev": "node scripts/dev.js",
     "build": "node scripts/build.js",
     "build-dts": "tsc -p tsconfig.build.json && rollup -c rollup.dts.config.js",
-    "size": "run-s size-global size-baseline",
-    "size-global": "node scripts/build.js vue runtime-dom -f global -p",
-    "size-baseline": "node scripts/build.js vue -f esm-bundler-runtime && node scripts/build.js runtime-dom runtime-core reactivity shared -f esm-bundler && cd packages/size-check && vite build && node brotli",
+    "size": "run-s \"size-*\" && tsx scripts/usage-size.ts",
+    "size-global": "node scripts/build.js vue runtime-dom -f global -p --size",
+    "size-esm-runtime": "node scripts/build.js vue -f esm-bundler-runtime",
+    "size-esm": "node scripts/build.js runtime-dom runtime-core reactivity shared -f esm-bundler",
     "check": "tsc --incremental --noEmit",
     "lint": "eslint --cache --ext .ts packages/*/{src,__tests__}/**.ts",
     "format": "prettier --write --cache \"**/*.[tj]s?(x)\"",
@@ -81,10 +82,12 @@
     "lint-staged": "^10.2.10",
     "lodash": "^4.17.15",
     "magic-string": "^0.30.0",
+    "markdown-table": "^3.0.3",
     "marked": "^4.0.10",
     "minimist": "^1.2.0",
     "npm-run-all": "^4.1.5",
     "prettier": "^3.0.1",
+    "pretty-bytes": "^6.1.1",
     "pug": "^3.0.1",
     "puppeteer": "~19.6.0",
     "rollup": "^3.26.0",
@@ -94,9 +97,10 @@
     "semver": "^7.3.2",
     "serve": "^12.0.0",
     "simple-git-hooks": "^2.8.1",
-    "terser": "^5.15.1",
+    "terser": "^5.19.2",
     "todomvc-app-css": "^2.3.0",
     "tslib": "^2.5.0",
+    "tsx": "^3.12.7",
     "typescript": "^5.1.6",
     "vite": "^4.3.0",
     "vitest": "^0.30.1"
diff --git a/packages/size-check/README.md b/packages/size-check/README.md
deleted file mode 100644
index 23cf1899eaf..00000000000
--- a/packages/size-check/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Size Check
-
-This package is private and is used for checking the baseline runtime size after tree-shaking (with only the bare minimal code required to render something to the screen).
diff --git a/packages/size-check/brotli.js b/packages/size-check/brotli.js
deleted file mode 100644
index f9dedac0b1c..00000000000
--- a/packages/size-check/brotli.js
+++ /dev/null
@@ -1,6 +0,0 @@
-const { brotliCompressSync } = require('zlib')
-
-const file = require('fs').readFileSync('dist/index.js')
-const compressed = brotliCompressSync(file)
-const compressedSize = (compressed.length / 1024).toFixed(2) + 'kb'
-console.log(`brotli: ${compressedSize}`)
diff --git a/packages/size-check/package.json b/packages/size-check/package.json
deleted file mode 100644
index 1f9fba88594..00000000000
--- a/packages/size-check/package.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
-  "name": "@vue/size-check",
-  "version": "3.3.4",
-  "private": true,
-  "scripts": {
-    "build": "vite build"
-  },
-  "dependencies": {
-    "vue": "workspace:*"
-  }
-}
diff --git a/packages/size-check/src/index.ts b/packages/size-check/src/index.ts
deleted file mode 100644
index ad3b68a5cc1..00000000000
--- a/packages/size-check/src/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { h, createApp } from 'vue'
-
-// The bare minimum code required for rendering something to the screen
-createApp({
-  render: () => h('div', 'hello world!')
-}).mount('#app')
diff --git a/packages/size-check/vite.config.js b/packages/size-check/vite.config.js
deleted file mode 100644
index 73721f95910..00000000000
--- a/packages/size-check/vite.config.js
+++ /dev/null
@@ -1,15 +0,0 @@
-export default {
-  define: {
-    __VUE_PROD_DEVTOOLS__: false,
-    __VUE_OPTIONS_API__: true
-  },
-  build: {
-    rollupOptions: {
-      input: ['src/index.ts'],
-      output: {
-        entryFileNames: `[name].js`
-      }
-    },
-    minify: 'terser'
-  }
-}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 378cc5749d4..7c6e2a198c5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -86,6 +86,9 @@ importers:
       magic-string:
         specifier: ^0.30.0
         version: 0.30.0
+      markdown-table:
+        specifier: ^3.0.3
+        version: 3.0.3
       marked:
         specifier: ^4.0.10
         version: 4.3.0
@@ -98,6 +101,9 @@ importers:
       prettier:
         specifier: ^3.0.1
         version: 3.0.1
+      pretty-bytes:
+        specifier: ^6.1.1
+        version: 6.1.1
       pug:
         specifier: ^3.0.1
         version: 3.0.2
@@ -126,23 +132,26 @@ importers:
         specifier: ^2.8.1
         version: 2.8.1
       terser:
-        specifier: ^5.15.1
-        version: 5.18.2
+        specifier: ^5.19.2
+        version: 5.19.2
       todomvc-app-css:
         specifier: ^2.3.0
         version: 2.4.2
       tslib:
         specifier: ^2.5.0
         version: 2.6.0
+      tsx:
+        specifier: ^3.12.7
+        version: 3.12.7
       typescript:
         specifier: ^5.1.6
         version: 5.1.6
       vite:
         specifier: ^4.3.0
-        version: 4.3.1(@types/node@16.18.38)(terser@5.18.2)
+        version: 4.3.1(@types/node@16.18.38)(terser@5.19.2)
       vitest:
         specifier: ^0.30.1
-        version: 0.30.1(jsdom@21.1.0)(terser@5.18.2)
+        version: 0.30.1(jsdom@21.1.0)(terser@5.19.2)
 
   packages/compiler-core:
     dependencies:
@@ -354,12 +363,6 @@ importers:
 
   packages/shared: {}
 
-  packages/size-check:
-    dependencies:
-      vue:
-        specifier: workspace:*
-        version: link:../vue
-
   packages/template-explorer:
     dependencies:
       monaco-editor:
@@ -422,11 +425,12 @@ packages:
       '@babel/highlight': 7.22.5
     dev: true
 
-  /@babel/code-frame@7.22.5:
-    resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==}
+  /@babel/code-frame@7.22.10:
+    resolution: {integrity: sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/highlight': 7.22.5
+      '@babel/highlight': 7.22.10
+      chalk: 2.4.2
     dev: true
 
   /@babel/compat-data@7.21.0:
@@ -567,6 +571,15 @@ packages:
       - supports-color
     dev: true
 
+  /@babel/highlight@7.22.10:
+    resolution: {integrity: sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/helper-validator-identifier': 7.22.5
+      chalk: 2.4.2
+      js-tokens: 4.0.0
+    dev: true
+
   /@babel/highlight@7.22.5:
     resolution: {integrity: sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==}
     engines: {node: '>=6.9.0'}
@@ -587,7 +600,7 @@ packages:
     resolution: {integrity: sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/code-frame': 7.18.6
+      '@babel/code-frame': 7.22.10
       '@babel/parser': 7.21.3
       '@babel/types': 7.21.3
     dev: true
@@ -596,7 +609,7 @@ packages:
     resolution: {integrity: sha512-XLyopNeaTancVitYZe2MlUEvgKb6YVVPXzofHgqHijCImG33b/uTurMS488ht/Hbsb2XK3U2BnSTxKVNGV3nGQ==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/code-frame': 7.22.5
+      '@babel/code-frame': 7.22.10
       '@babel/generator': 7.21.3
       '@babel/helper-environment-visitor': 7.18.9
       '@babel/helper-function-name': 7.21.0
@@ -618,6 +631,27 @@ packages:
       '@babel/helper-validator-identifier': 7.19.1
       to-fast-properties: 2.0.0
 
+  /@esbuild-kit/cjs-loader@2.4.2:
+    resolution: {integrity: sha512-BDXFbYOJzT/NBEtp71cvsrGPwGAMGRB/349rwKuoxNSiKjPraNNnlK6MIIabViCjqZugu6j+xeMDlEkWdHHJSg==}
+    dependencies:
+      '@esbuild-kit/core-utils': 3.1.0
+      get-tsconfig: 4.7.0
+    dev: true
+
+  /@esbuild-kit/core-utils@3.1.0:
+    resolution: {integrity: sha512-Uuk8RpCg/7fdHSceR1M6XbSZFSuMrxcePFuGgyvsBn+u339dk5OeL4jv2EojwTN2st/unJGsVm4qHWjWNmJ/tw==}
+    dependencies:
+      esbuild: 0.17.19
+      source-map-support: 0.5.21
+    dev: true
+
+  /@esbuild-kit/esm-loader@2.5.5:
+    resolution: {integrity: sha512-Qwfvj/qoPbClxCRNuac1Du01r9gvNOT+pMYtJDapfB1eoGN1YlJ1BixLyL9WVENRx5RXgNLdfYdx/CuswlGhMw==}
+    dependencies:
+      '@esbuild-kit/core-utils': 3.1.0
+      get-tsconfig: 4.7.0
+    dev: true
+
   /@esbuild/android-arm64@0.17.19:
     resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==}
     engines: {node: '>=12'}
@@ -1078,11 +1112,25 @@ packages:
       '@jridgewell/trace-mapping': 0.3.17
     dev: true
 
+  /@jridgewell/gen-mapping@0.3.3:
+    resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==}
+    engines: {node: '>=6.0.0'}
+    dependencies:
+      '@jridgewell/set-array': 1.1.2
+      '@jridgewell/sourcemap-codec': 1.4.15
+      '@jridgewell/trace-mapping': 0.3.19
+    dev: true
+
   /@jridgewell/resolve-uri@3.1.0:
     resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==}
     engines: {node: '>=6.0.0'}
     dev: true
 
+  /@jridgewell/resolve-uri@3.1.1:
+    resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==}
+    engines: {node: '>=6.0.0'}
+    dev: true
+
   /@jridgewell/set-array@1.1.2:
     resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==}
     engines: {node: '>=6.0.0'}
@@ -1091,13 +1139,17 @@ packages:
   /@jridgewell/source-map@0.3.5:
     resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==}
     dependencies:
-      '@jridgewell/gen-mapping': 0.3.2
-      '@jridgewell/trace-mapping': 0.3.17
+      '@jridgewell/gen-mapping': 0.3.3
+      '@jridgewell/trace-mapping': 0.3.19
     dev: true
 
   /@jridgewell/sourcemap-codec@1.4.14:
     resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==}
 
+  /@jridgewell/sourcemap-codec@1.4.15:
+    resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
+    dev: true
+
   /@jridgewell/trace-mapping@0.3.17:
     resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==}
     dependencies:
@@ -1105,6 +1157,13 @@ packages:
       '@jridgewell/sourcemap-codec': 1.4.14
     dev: true
 
+  /@jridgewell/trace-mapping@0.3.19:
+    resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==}
+    dependencies:
+      '@jridgewell/resolve-uri': 3.1.1
+      '@jridgewell/sourcemap-codec': 1.4.15
+    dev: true
+
   /@jspm/core@2.0.1:
     resolution: {integrity: sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==}
     dev: true
@@ -1233,7 +1292,7 @@ packages:
       rollup: 3.26.2
       serialize-javascript: 6.0.1
       smob: 0.0.6
-      terser: 5.18.2
+      terser: 5.19.2
     dev: true
 
   /@rollup/pluginutils@5.0.2(rollup@3.26.2):
@@ -1464,7 +1523,7 @@ packages:
       istanbul-lib-source-maps: 4.0.1
       istanbul-reports: 3.1.5
       test-exclude: 6.0.0
-      vitest: 0.30.1(jsdom@21.1.0)(terser@5.18.2)
+      vitest: 0.30.1(jsdom@21.1.0)(terser@5.19.2)
     transitivePeerDependencies:
       - supports-color
     dev: true
@@ -1568,6 +1627,12 @@ packages:
     hasBin: true
     dev: true
 
+  /acorn@8.10.0:
+    resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==}
+    engines: {node: '>=0.4.0'}
+    hasBin: true
+    dev: true
+
   /acorn@8.8.2:
     resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==}
     engines: {node: '>=0.4.0'}
@@ -3066,6 +3131,12 @@ packages:
       get-intrinsic: 1.2.0
     dev: true
 
+  /get-tsconfig@4.7.0:
+    resolution: {integrity: sha512-pmjiZ7xtB8URYm74PlGJozDNyhvsVLUcpBa8DZBG3bWHwaHa9bPiRpiSfovw+fjhwONSCWKRyk+JQHEGZmMrzw==}
+    dependencies:
+      resolve-pkg-maps: 1.0.0
+    dev: true
+
   /git-raw-commits@2.0.11:
     resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==}
     engines: {node: '>=10'}
@@ -3991,6 +4062,10 @@ packages:
     engines: {node: '>=8'}
     dev: true
 
+  /markdown-table@3.0.3:
+    resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==}
+    dev: true
+
   /marked@4.3.0:
     resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==}
     engines: {node: '>= 12'}
@@ -4414,7 +4489,7 @@ packages:
     resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
     engines: {node: '>=8'}
     dependencies:
-      '@babel/code-frame': 7.22.5
+      '@babel/code-frame': 7.22.10
       error-ex: 1.3.2
       json-parse-even-better-errors: 2.3.1
       lines-and-columns: 1.2.4
@@ -4627,6 +4702,11 @@ packages:
     hasBin: true
     dev: true
 
+  /pretty-bytes@6.1.1:
+    resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==}
+    engines: {node: ^14.13.1 || >=16.0.0}
+    dev: true
+
   /pretty-format@27.5.1:
     resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
     engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@@ -4963,6 +5043,10 @@ packages:
     engines: {node: '>=4'}
     dev: true
 
+  /resolve-pkg-maps@1.0.0:
+    resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+    dev: true
+
   /resolve@1.22.1:
     resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==}
     hasBin: true
@@ -5007,7 +5091,7 @@ packages:
       rollup: 3.26.2
       typescript: 5.1.6
     optionalDependencies:
-      '@babel/code-frame': 7.22.5
+      '@babel/code-frame': 7.22.10
     dev: true
 
   /rollup-plugin-esbuild@5.0.0(esbuild@0.17.19)(rollup@3.26.2):
@@ -5492,6 +5576,17 @@ packages:
       source-map-support: 0.5.21
     dev: true
 
+  /terser@5.19.2:
+    resolution: {integrity: sha512-qC5+dmecKJA4cpYxRa5aVkKehYsQKc+AHeKl0Oe62aYjBL8ZA33tTljktDHJSaxxMnbI5ZYw+o/S2DxxLu8OfA==}
+    engines: {node: '>=10'}
+    hasBin: true
+    dependencies:
+      '@jridgewell/source-map': 0.3.5
+      acorn: 8.10.0
+      commander: 2.20.3
+      source-map-support: 0.5.21
+    dev: true
+
   /test-exclude@6.0.0:
     resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
     engines: {node: '>=8'}
@@ -5609,6 +5704,17 @@ packages:
       typescript: 5.1.6
     dev: true
 
+  /tsx@3.12.7:
+    resolution: {integrity: sha512-C2Ip+jPmqKd1GWVQDvz/Eyc6QJbGfE7NrR3fx5BpEHMZsEHoIxHL1j+lKdGobr8ovEyqeNkPLSKp6SCSOt7gmw==}
+    hasBin: true
+    dependencies:
+      '@esbuild-kit/cjs-loader': 2.4.2
+      '@esbuild-kit/core-utils': 3.1.0
+      '@esbuild-kit/esm-loader': 2.5.5
+    optionalDependencies:
+      fsevents: 2.3.2
+    dev: true
+
   /type-check@0.3.2:
     resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==}
     engines: {node: '>= 0.8.0'}
@@ -5758,7 +5864,7 @@ packages:
     engines: {node: '>= 0.8'}
     dev: true
 
-  /vite-node@0.30.1(@types/node@16.18.38)(terser@5.18.2):
+  /vite-node@0.30.1(@types/node@16.18.38)(terser@5.19.2):
     resolution: {integrity: sha512-vTikpU/J7e6LU/8iM3dzBo8ZhEiKZEKRznEMm+mJh95XhWaPrJQraT/QsT2NWmuEf+zgAoMe64PKT7hfZ1Njmg==}
     engines: {node: '>=v14.18.0'}
     hasBin: true
@@ -5768,7 +5874,7 @@ packages:
       mlly: 1.2.0
       pathe: 1.1.0
       picocolors: 1.0.0
-      vite: 4.3.1(@types/node@16.18.38)(terser@5.18.2)
+      vite: 4.3.1(@types/node@16.18.38)(terser@5.19.2)
     transitivePeerDependencies:
       - '@types/node'
       - less
@@ -5779,7 +5885,7 @@ packages:
       - terser
     dev: true
 
-  /vite@4.3.1(@types/node@16.18.38)(terser@5.18.2):
+  /vite@4.3.1(@types/node@16.18.38)(terser@5.19.2):
     resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==}
     engines: {node: ^14.18.0 || >=16.0.0}
     hasBin: true
@@ -5808,7 +5914,7 @@ packages:
       esbuild: 0.17.19
       postcss: 8.4.21
       rollup: 3.26.2
-      terser: 5.18.2
+      terser: 5.19.2
     optionalDependencies:
       fsevents: 2.3.2
     dev: true
@@ -5850,7 +5956,7 @@ packages:
       fsevents: 2.3.2
     dev: true
 
-  /vitest@0.30.1(jsdom@21.1.0)(terser@5.18.2):
+  /vitest@0.30.1(jsdom@21.1.0)(terser@5.19.2):
     resolution: {integrity: sha512-y35WTrSTlTxfMLttgQk4rHcaDkbHQwDP++SNwPb+7H8yb13Q3cu2EixrtHzF27iZ8v0XCciSsLg00RkPAzB/aA==}
     engines: {node: '>=v14.18.0'}
     hasBin: true
@@ -5905,8 +6011,8 @@ packages:
       strip-literal: 1.0.1
       tinybench: 2.4.0
       tinypool: 0.4.0
-      vite: 4.3.1(@types/node@16.18.38)(terser@5.18.2)
-      vite-node: 0.30.1(@types/node@16.18.38)(terser@5.18.2)
+      vite: 4.3.1(@types/node@16.18.38)(terser@5.19.2)
+      vite-node: 0.30.1(@types/node@16.18.38)(terser@5.19.2)
       why-is-node-running: 2.2.2
     transitivePeerDependencies:
       - less
diff --git a/scripts/aliases.js b/scripts/aliases.js
index 95e3016322c..34a7c643557 100644
--- a/scripts/aliases.js
+++ b/scripts/aliases.js
@@ -19,12 +19,7 @@ const entries = {
   '@vue/compat': resolveEntryForPkg('vue-compat')
 }
 
-const nonSrcPackages = [
-  'sfc-playground',
-  'size-check',
-  'template-explorer',
-  'dts-test'
-]
+const nonSrcPackages = ['sfc-playground', 'template-explorer', 'dts-test']
 
 for (const dir of dirs) {
   const key = `@vue/${dir}`
diff --git a/scripts/build.js b/scripts/build.js
index 75a619046be..1f8af65017d 100644
--- a/scripts/build.js
+++ b/scripts/build.js
@@ -27,6 +27,7 @@ import { cpus } from 'node:os'
 import { createRequire } from 'node:module'
 import { targets as allTargets, fuzzyMatchTarget } from './utils.js'
 import { scanEnums } from './const-enum.js'
+import prettyBytes from 'pretty-bytes'
 
 const require = createRequire(import.meta.url)
 const args = minimist(process.argv.slice(2))
@@ -38,18 +39,22 @@ const buildTypes = args.withTypes || args.t
 const sourceMap = args.sourcemap || args.s
 const isRelease = args.release
 const buildAllMatching = args.all || args.a
+const writeSize = args.size
 const commit = execa.sync('git', ['rev-parse', 'HEAD']).stdout.slice(0, 7)
 
+const sizeDir = path.resolve('temp/size')
+
 run()
 
 async function run() {
+  if (writeSize) await fs.mkdir(sizeDir, { recursive: true })
   const removeCache = scanEnums()
   try {
     const resolvedTargets = targets.length
       ? fuzzyMatchTarget(targets, buildAllMatching)
       : allTargets
     await buildAll(resolvedTargets)
-    checkAllSizes(resolvedTargets)
+    await checkAllSizes(resolvedTargets)
     if (buildTypes) {
       await execa(
         'pnpm',
@@ -129,39 +134,52 @@ async function build(target) {
   )
 }
 
-function checkAllSizes(targets) {
+async function checkAllSizes(targets) {
   if (devOnly || (formats && !formats.includes('global'))) {
     return
   }
   console.log()
   for (const target of targets) {
-    checkSize(target)
+    await checkSize(target)
   }
   console.log()
 }
 
-function checkSize(target) {
+async function checkSize(target) {
   const pkgDir = path.resolve(`packages/${target}`)
-  checkFileSize(`${pkgDir}/dist/${target}.global.prod.js`)
+  await checkFileSize(`${pkgDir}/dist/${target}.global.prod.js`)
   if (!formats || formats.includes('global-runtime')) {
-    checkFileSize(`${pkgDir}/dist/${target}.runtime.global.prod.js`)
+    await checkFileSize(`${pkgDir}/dist/${target}.runtime.global.prod.js`)
   }
 }
 
-function checkFileSize(filePath) {
+async function checkFileSize(filePath) {
   if (!existsSync(filePath)) {
     return
   }
-  const file = readFileSync(filePath)
-  const minSize = (file.length / 1024).toFixed(2) + 'kb'
+  const file = await fs.readFile(filePath)
+  const fileName = path.basename(filePath)
+
   const gzipped = gzipSync(file)
-  const gzippedSize = (gzipped.length / 1024).toFixed(2) + 'kb'
-  const compressed = brotliCompressSync(file)
-  // @ts-ignore
-  const compressedSize = (compressed.length / 1024).toFixed(2) + 'kb'
+  const brotli = brotliCompressSync(file)
+
   console.log(
-    `${chalk.gray(
-      chalk.bold(path.basename(filePath))
-    )} min:${minSize} / gzip:${gzippedSize} / brotli:${compressedSize}`
+    `${chalk.gray(chalk.bold(fileName))} min:${prettyBytes(
+      file.length
+    )} / gzip:${prettyBytes(gzipped.length)} / brotli:${prettyBytes(
+      brotli.length
+    )}`
   )
+
+  if (writeSize)
+    await fs.writeFile(
+      path.resolve(sizeDir, `${fileName}.json`),
+      JSON.stringify({
+        file: fileName,
+        size: file.length,
+        gzip: gzipped.length,
+        brotli: brotli.length
+      }),
+      'utf-8'
+    )
 }
diff --git a/scripts/size-report.ts b/scripts/size-report.ts
new file mode 100644
index 00000000000..56e4491a19c
--- /dev/null
+++ b/scripts/size-report.ts
@@ -0,0 +1,105 @@
+import path from 'node:path'
+import { markdownTable } from 'markdown-table'
+import prettyBytes from 'pretty-bytes'
+import { readdir } from 'node:fs/promises'
+import { existsSync } from 'node:fs'
+
+interface SizeResult {
+  size: number
+  gzip: number
+  brotli: number
+}
+
+interface BundleResult extends SizeResult {
+  file: string
+}
+
+type UsageResult = Record<string, SizeResult & { name: string }>
+
+const currDir = path.resolve('temp/size')
+const prevDir = path.resolve('temp/size-prev')
+let output = '## Size Report\n\n'
+const sizeHeaders = ['Size', 'Gzip', 'Brotli']
+
+run()
+
+async function run() {
+  await renderFiles()
+  await renderUsages()
+
+  process.stdout.write(output)
+}
+
+async function renderFiles() {
+  const filterFiles = (files: string[]) =>
+    files.filter(file => !file.startsWith('_'))
+
+  const curr = filterFiles(await readdir(currDir))
+  const prev = existsSync(prevDir) ? filterFiles(await readdir(prevDir)) : []
+  const fileList = new Set([...curr, ...prev])
+
+  const rows: string[][] = []
+  for (const file of fileList) {
+    const currPath = path.resolve(currDir, file)
+    const prevPath = path.resolve(prevDir, file)
+
+    const curr = await importJSON<BundleResult>(currPath)
+    const prev = await importJSON<BundleResult>(prevPath)
+    const fileName = curr?.file || prev?.file || ''
+
+    if (!curr) {
+      rows.push([`~~${fileName}~~`])
+    } else
+      rows.push([
+        fileName,
+        `${prettyBytes(curr.size)}${getDiff(curr.size, prev?.size)}`,
+        `${prettyBytes(curr.gzip)}${getDiff(curr.gzip, prev?.gzip)}`,
+        `${prettyBytes(curr.brotli)}${getDiff(curr.brotli, prev?.brotli)}`
+      ])
+  }
+
+  output += '### Bundles\n\n'
+  output += markdownTable([['File', ...sizeHeaders], ...rows])
+  output += '\n\n'
+}
+
+async function renderUsages() {
+  const curr = (await importJSON<UsageResult>(
+    path.resolve(currDir, '_usages.json')
+  ))!
+  const prev = await importJSON<UsageResult>(
+    path.resolve(prevDir, '_usages.json')
+  )
+  output += '\n### Usages\n\n'
+
+  const data = Object.values(curr)
+    .map(usage => {
+      const prevUsage = prev?.[usage.name]
+      const diffSize = getDiff(usage.size, prevUsage?.size)
+      const diffGzipped = getDiff(usage.gzip, prevUsage?.gzip)
+      const diffBrotli = getDiff(usage.brotli, prevUsage?.brotli)
+
+      return [
+        usage.name,
+        `${prettyBytes(usage.size)}${diffSize}`,
+        `${prettyBytes(usage.gzip)}${diffGzipped}`,
+        `${prettyBytes(usage.brotli)}${diffBrotli}`
+      ]
+    })
+    .filter((usage): usage is string[] => !!usage)
+
+  output += `${markdownTable([['Name', ...sizeHeaders], ...data])}\n\n`
+}
+
+async function importJSON<T>(path: string): Promise<T | undefined> {
+  if (!existsSync(path)) return undefined
+  return (await import(path, { assert: { type: 'json' } })).default
+}
+
+function getDiff(curr: number, prev?: number) {
+  if (prev === undefined) return ''
+  const diff = curr - prev
+  if (diff === 0) return ''
+  const sign = diff > 0 ? '+' : ''
+  return ` (**${sign}${prettyBytes(diff)}**)`
+}
diff --git a/scripts/usage-size.ts b/scripts/usage-size.ts
new file mode 100644
index 00000000000..1a1013b7847
--- /dev/null
+++ b/scripts/usage-size.ts
@@ -0,0 +1,99 @@
+import { mkdir, writeFile } from 'fs/promises'
+import path from 'node:path'
+import { rollup } from 'rollup'
+import nodeResolve from '@rollup/plugin-node-resolve'
+import { minify } from 'terser'
+import replace from '@rollup/plugin-replace'
+import { brotliCompressSync, gzipSync } from 'node:zlib'
+
+const sizeDir = path.resolve('temp/size')
+const entry = path.resolve('./packages/vue/dist/vue.runtime.esm-bundler.js')
+
+interface Preset {
+  name: string
+  imports: string[]
+}
+
+const presets: Preset[] = [
+  { name: 'createApp', imports: ['createApp'] },
+  { name: 'createSSRApp', imports: ['createSSRApp'] },
+  { name: 'defineCustomElement', imports: ['defineCustomElement'] },
+  {
+    name: 'overall',
+    imports: [
+      'createApp',
+      'ref',
+      'watch',
+      'Transition',
+      'KeepAlive',
+      'Suspense'
+    ]
+  }
+]
+
+main()
+
+async function main() {
+  const tasks: ReturnType<typeof generateBundle>[] = []
+  for (const preset of presets) {
+    tasks.push(generateBundle(preset))
+  }
+
+  const results = Object.fromEntries(
+    (await Promise.all(tasks)).map(r => [r.name, r])
+  )
+
+  await mkdir(sizeDir, { recursive: true })
+  await writeFile(
+    path.resolve(sizeDir, '_usages.json'),
+    JSON.stringify(results),
+    'utf-8'
+  )
+}
+
+async function generateBundle(preset: Preset) {
+  const id = 'virtual:entry'
+  const content = `export { ${preset.imports.join(', ')} } from '${entry}'`
+  const result = await rollup({
+    input: id,
+    plugins: [
+      {
+        name: 'usage-size-plugin',
+        resolveId(_id) {
+          if (_id === id) return id
+          return null
+        },
+        load(_id) {
+          if (_id === id) return content
+        }
+      },
+      nodeResolve(),
+      replace({
+        'process.env.NODE_ENV': '"production"',
+        __VUE_PROD_DEVTOOLS__: 'false',
+        __VUE_OPTIONS_API__: 'true',
+        preventAssignment: true
+      })
+    ]
+  })
+
+  const generated = await result.generate({})
+  const bundled = generated.output[0].code
+  const minified = (
+    await minify(bundled, {
+      module: true,
+      toplevel: true
+    })
+  ).code!
+
+  const size = minified.length
+  const gzip = gzipSync(minified).length
+  const brotli = brotliCompressSync(minified).length
+
+  return {
+    name: preset.name,
+    size,
+    gzip,
+    brotli
+  }
+}
diff --git a/tsconfig.build.json b/tsconfig.build.json
index 8b7749b858b..89aaa2278f4 100644
--- a/tsconfig.build.json
+++ b/tsconfig.build.json
@@ -9,7 +9,6 @@
     "packages/runtime-test",
     "packages/template-explorer",
     "packages/sfc-playground",
-    "packages/size-check",
     "packages/dts-test"
   ]
 }