diff --git a/.github/actions/calculate-prefetch-matrix/action.yml b/.github/actions/calculate-prefetch-matrix/action.yml
new file mode 100644
index 0000000000000000000000000000000000000000..18bc88e2cf16a1ff8a4fa19a4cab31680b942211
--- /dev/null
+++ b/.github/actions/calculate-prefetch-matrix/action.yml
@@ -0,0 +1,81 @@
+# This is the composite action:
+#   https://docs.github.com/en/actions/creating-actions/creating-a-composite-action
+#
+# Composite actions have some limitations:
+#   - many contexts are unavailable, e.g. `runner`
+#   - `env` can be specified per-step only
+#   - if `run` is used, `shell` must be explicitly specified
+name: 'Calculate matrix for `node_modules` prefetch'
+inputs:
+  repo:
+    description: 'Repository name'
+    required: true
+  token:
+    description: 'GitHub token'
+    required: true
+  node-version:
+    description: 'Node version'
+    required: true
+  hash:
+    description: 'Yarn lock hash'
+    required: true
+outputs:
+  matrix:
+    description: 'Matrix of OSes to prefetch `node_modules` for'
+    value: ${{ steps.os-matrix-prefetch.outputs.os-matrix-prefetch }}
+runs:
+  using: 'composite'
+  steps:
+    - name: Calculate cache keys
+      id: cache-keys
+      env:
+        LOCK_HASH: ${{ steps.yarn-lock-hash.outputs.yarn-lock-hash }}
+      shell: bash
+      run: |
+        echo 'macos-cache-key=node-modules-macOS-${{ inputs.node-version }}-${{ inputs.hash }}' >> "$GITHUB_OUTPUT"
+        echo 'windows-cache-key=node-modules-Windows-${{ inputs.node-version }}-${{ inputs.hash }}' >> "$GITHUB_OUTPUT"
+
+    - name: Fetch available cache keys
+      id: caches-list
+      env:
+        GH_TOKEN: ${{ inputs.token }}
+        GH_REPO: ${{ inputs.repo }}
+        CACHES_URL: /repos/{owner}/{repo}/actions/caches
+        JQ_FILTER: >-
+          "keys=" + ([.[].actions_caches[].key] | tostring)
+      shell: bash
+      run: |
+        gh api ${{ env.CACHES_URL }} --paginate | jq -rcs '${{ env.JQ_FILTER }}' >> "$GITHUB_OUTPUT"
+
+    - name: Calculate cache misses for Windows and MacOS
+      id: modules-caches
+      env:
+        ACTIONS_CACHE_KEYS: ${{ steps.caches-list.outputs.keys }}
+        MACOS_CACHE_KEY: ${{ steps.cache-keys.outputs.macos-cache-key }}
+        WINDOWS_CACHE_KEY: ${{ steps.cache-keys.outputs.windows-cache-key }}
+      shell: bash
+      run: |
+        echo 'macos=${{
+          !contains(fromJSON(env.ACTIONS_CACHE_KEYS), env.MACOS_CACHE_KEY) && 'true' || ''
+        }}' >> "$GITHUB_OUTPUT"
+        echo 'windows=${{
+          !contains(fromJSON(env.ACTIONS_CACHE_KEYS), env.WINDOWS_CACHE_KEY) && 'true' || ''
+        }}' >> "$GITHUB_OUTPUT"
+
+    - name: Dispatch `node_modules` prefetch for MacOS and Windows
+      id: os-matrix-prefetch
+      env:
+        MACOS_MISS: ${{ steps.modules-caches.outputs.macos }}
+        WINDOWS_MISS: ${{ steps.modules-caches.outputs.windows }}
+        PREFETCH_MAC_ONLY: '["macos-latest"]'
+        PREFETCH_WINDOWS_ONLY: '["windows-latest"]'
+        PREFETCH_BOTH: '["macos-latest", "windows-latest"]'
+        PREFETCH_FALLBACK: '["ubuntu-latest"]'
+      shell: bash
+      run: |
+        echo 'os-matrix-prefetch=${{
+          (env.OS_MATRIX_IS_FULL && env.WINDOWS_MISS && env.MACOS_MISS && env.PREFETCH_BOTH) ||
+          (env.OS_MATRIX_IS_FULL && env.MACOS_MISS && env.PREFETCH_MAC_ONLY) ||
+          (env.OS_MATRIX_IS_FULL && env.WINDOWS_MISS && env.PREFETCH_WINDOWS_ONLY) ||
+          env.PREFETCH_FALLBACK
+        }}' >> "$GITHUB_OUTPUT"
diff --git a/.github/actions/setup-node/action.yml b/.github/actions/setup-node/action.yml
index 7327949214e1a854146468aaec001c78dd59324f..3b22954036a1e6e66b7245aa308dfc30347c29d3 100644
--- a/.github/actions/setup-node/action.yml
+++ b/.github/actions/setup-node/action.yml
@@ -40,8 +40,8 @@ runs:
       with:
         timeout_minutes: 10
         max_attempts: 3
-        command: yarn --silent install --frozen-lockfile --ignore-scripts
+        command: yarn install --frozen-lockfile --ignore-scripts
 
     - name: Run scripts
       shell: bash
-      run: yarn --silent prepare
+      run: yarn prepare
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 87832d462a4e1b2dc1ff430054f5c410b82236c5..5f38e3a20937d24cb47569db4ffcdb66b08528e8 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -27,6 +27,12 @@ env:
   DEFAULT_BRANCH: main
   NODE_VERSION: 18
   DRY_RUN: true
+  SPARSE_CHECKOUT: |-
+    .github/actions/
+    data/
+    tools/
+    package.json
+    yarn.lock
 
 jobs:
   setup:
@@ -34,6 +40,8 @@ jobs:
 
     outputs:
       os-matrix: ${{ steps.os-matrix.outputs.os-matrix }}
+      os-matrix-is-full: ${{ steps.os-matrix-is-full.outputs.os-matrix-is-full }}
+      os-matrix-prefetch: ${{ steps.os-matrix-prefetch.outputs.matrix }}
 
     env:
       # Field required for GitHub CLI
@@ -46,27 +54,98 @@ jobs:
 
     steps:
       - name: Fetch PR data
-        if: ${{ env.PR }}
+        if: env.PR
         env:
           PR_URL: https://api.github.com/repos/{owner}/{repo}/pulls/${{ env.PR }}
           JQ_FILTER: >-
             "PR_LABELS=" + ([.labels[].name] | tostring)
         run: gh api ${{ env.PR_URL }} | jq -rc '${{ env.JQ_FILTER }}' >> "$GITHUB_ENV"
 
-      - name: Detect OS matrix
+      - name: Calculate `CI_FULLTEST` variable
+        run: |
+          echo 'CI_FULLTEST=${{
+            contains(fromJSON(env.PR_LABELS), 'ci:fulltest') && 'true' || ''
+          }}' >> "$GITHUB_ENV"
+
+      - name: Calculate `os-matrix-is-full` output
+        id: os-matrix-is-full
+        env:
+          IS_FULL: ${{ (!env.PR || env.CI_FULLTEST) && 'true' || '' }}
+        run: |
+          echo 'OS_MATRIX_IS_FULL=${{ env.IS_FULL }}' >> "$GITHUB_ENV"
+          echo 'os-matrix-is-full=${{ env.IS_FULL }}' >> "$GITHUB_OUTPUT"
+
+      - name: Calculate `os-matrix` output
         id: os-matrix
         env:
-          CI_FULLTEST: >-
-            ${{ contains(fromJSON(env.PR_LABELS), 'ci:fulltest') && 'true' || '' }}
           OS_ALL: '["ubuntu-latest", "macos-latest", "windows-latest"]'
           OS_LINUX_ONLY: '["ubuntu-latest"]'
-        run: >-
+        run: |
           echo 'os-matrix=${{
-            (!env.PR || env.CI_FULLTEST) && env.OS_ALL || env.OS_LINUX_ONLY
+            env.OS_MATRIX_IS_FULL && env.OS_ALL || env.OS_LINUX_ONLY
           }}' >> "$GITHUB_OUTPUT"
 
+      - name: Checkout code
+        uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+        with:
+          sparse-checkout: ${{ env.SPARSE_CHECKOUT }}
+
+      - name: Calculate `yarn-lock-hash` output
+        id: yarn-lock-hash
+        run: |
+          echo 'yarn-lock-hash=${{ hashFiles('yarn.lock') }}' >> "$GITHUB_OUTPUT"
+
+      - name: Calculate matrix for `node_modules` prefetch
+        uses: ./.github/actions/calculate-prefetch-matrix
+        id: os-matrix-prefetch
+        with:
+          repo: ${{ env.GH_REPO }}
+          token: ${{ env.GH_TOKEN }}
+          node-version: ${{ env.NODE_VERSION }}
+          hash: ${{ steps.yarn-lock-hash.outputs.yarn-lock-hash }}
+
+      - name: Prefetch modules for `ubuntu-latest`
+        id: setup-node
+        uses: ./.github/actions/setup-node
+        with:
+          node-version: ${{ env.NODE_VERSION }}
+          os: ${{ runner.os }}
+
+  prefetch:
+    needs: [setup]
+    # We can't use `if: needs.setup.outputs.os-matrix-is-full` here,
+    # as it will lead to further complications that aren't solvable
+    # with current GitHub Actions feature set.
+    #
+    # Although this job sometimes may act as short-lived `no-op`,
+    # it's actually the best option available.
+
+    strategy:
+      matrix:
+        os: ${{ fromJSON(needs.setup.outputs.os-matrix-prefetch) }}
+        node-version: [18]
+
+    runs-on: ${{ matrix.os }}
+
+    timeout-minutes: 10
+
+    steps:
+      - name: Checkout code
+        if: needs.setup.outputs.os-matrix-is-full && runner.os != 'Linux'
+        uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+        with:
+          sparse-checkout: ${{ env.SPARSE_CHECKOUT }}
+
+      - name: Setup Node.js
+        if: needs.setup.outputs.os-matrix-is-full && runner.os != 'Linux'
+        uses: ./.github/actions/setup-node
+        with:
+          node-version: ${{ env.NODE_VERSION }}
+          os: ${{ runner.os }}
+
   test:
-    needs: setup
+    needs: [setup, prefetch]
+
     name: ${{ matrix.node-version == 18 && format('test ({0})', matrix.os) || format('test ({0}, node-{1})', matrix.os, matrix.node-version) }}
     runs-on: ${{ matrix.os }}
 
@@ -125,6 +204,7 @@ jobs:
         run: yarn test-e2e
 
   lint:
+    needs: [setup]
     runs-on: ubuntu-latest
 
     # lint shouldn't need more than 10 min