diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a0128ec733a0389db2d13cefe704236af6fd0917..fd0e3346fda065a5389572c349c269bb28a60454 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -57,6 +57,7 @@ workflow:
 
 variables:
   _CONFIG_DOCKERHUB_ROOT: "drupalci"
+  CACHE_TARGET: "${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}${CI_COMMIT_BRANCH}"
   # Let composer know what self.version means.
   COMPOSER_ROOT_VERSION: "${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}${CI_COMMIT_BRANCH}-dev"
   COMPOSER_ALLOW_SUPERUSER: 1
@@ -101,7 +102,24 @@ default:
   rules:
     - if: $PERFORMANCE_TEST != "1"
 
-
+.phpstan-cache: &phpstan-cache
+  # Get the phpstan cache file from the artifacts of the latest successful
+  # job from the target branch. Allow the job to proceed and pass if the file
+  # doesn't exist.
+  - mkdir core/phpstan-tmp
+  - 'curl --location --output core/phpstan-tmp/resultCache.php "https://git.drupalcode.org/api/v4/projects/$CI_PROJECT_ID/jobs/artifacts/{$CACHE_TARGET}/raw/core/phpstan-tmp/resultCache.php?job=Lint%20cache%20warming&job_token=$CI_JOB_TOKEN" || true'
+
+.cspell-cache: &cspell-cache
+  # Fetch the cspell cache from the artifacts of the latest successful job from
+  # the target branch. Allow the job to proceed and pass if the file doesn't
+  # exist.
+  - 'curl --location --output core/.cspellcache "https://git.drupalcode.org/api/v4/projects/$CI_PROJECT_ID/jobs/artifacts/{$CACHE_TARGET}/raw/core/.cspellcache?job=Lint%20cache%20warming&job_token=$CI_JOB_TOKEN" || true'
+
+.core-spellcheck: &core-spellcheck
+  - cd core
+  - corepack enable
+  - yarn install
+  - yarn run spellcheck:core --no-must-find-files --cache --cache-strategy content
 ################
 # Stages
 #
@@ -239,12 +257,35 @@ default:
 # Lint Jobs
 ################
 
+
+'Lint cache warming':
+  <<: [ *default-job-settings-lint ]
+  stage: 🪄 Lint
+  rules:
+    - if: $CI_PIPELINE_SOURCE == "push" && $CI_PROJECT_ROOT_NAMESPACE == "project"
+    - when: manual
+      allow_failure: true
+  variables:
+    KUBERNETES_CPU_REQUEST: "4"
+  script:
+    - *phpstan-cache
+    - *cspell-cache
+    - composer install
+    - vendor/bin/phpstan --version
+    - php vendor/bin/phpstan -vvv analyze --configuration=./core/phpstan.neon.dist
+    - *core-spellcheck
+  artifacts:
+    paths:
+      - core/phpstan-tmp/resultCache.php
+      - core/.cspellcache
+
 '🧹 PHP Static Analysis (phpstan)':
   <<: [ *default-job-settings-lint ]
   stage: 🪄 Lint
   variables:
-    KUBERNETES_CPU_REQUEST: "16"
+    KUBERNETES_CPU_REQUEST: "4"
   script:
+    - *phpstan-cache
     - composer validate
     - composer install --optimize-autoloader
     - if [ -n "$COMPOSER_UPDATE" ]; then
@@ -254,8 +295,8 @@ default:
     - vendor/bin/phpstan --version
     # Rely on PHPStan caching to execute analysis multiple times without performance drawback.
     # Output a copy in junit.
-    - php vendor/bin/phpstan analyze --configuration=./core/phpstan.neon.dist --error-format=gitlab > phpstan-quality-report.json || EXIT_CODE=$?
-    - php vendor/bin/phpstan analyze --configuration=./core/phpstan.neon.dist --no-progress --error-format=junit > phpstan-junit.xml || true
+    - php vendor/bin/phpstan -vvv analyze --configuration=./core/phpstan.neon.dist --error-format=gitlab > phpstan-quality-report.json || EXIT_CODE=$?
+    - php vendor/bin/phpstan -vvv analyze --configuration=./core/phpstan.neon.dist --no-progress --error-format=junit > phpstan-junit.xml || true
     - |
       if [ -n "$EXIT_CODE" ]; then
         # Output a copy in plain text for human logs.
@@ -267,11 +308,11 @@ default:
       fi
 
   artifacts:
+    # Only store the baseline if the job fails.
+    when: on_failure
     reports:
       codequality: phpstan-quality-report.json
       junit: phpstan-junit.xml
-    # Only store the baseline if the job fails.
-    when: on_failure
     paths:
       - core/.phpstan-baseline.php
 
@@ -356,16 +397,8 @@ default:
   variables:
     KUBERNETES_CPU_REQUEST: "2"
   script:
-    - if [ -n "$CI_MERGE_REQUEST_TARGET_BRANCH_SHA" ]; then
-        echo "HEAD is $(git rev-parse HEAD). \$CI_MERGE_REQUEST_TARGET_BRANCH_SHA is ${CI_MERGE_REQUEST_TARGET_BRANCH_SHA}";
-      else
-        echo "HEAD is $(git rev-parse HEAD). \$CI_MERGE_REQUEST_DIFF_BASE_SHA is ${CI_MERGE_REQUEST_DIFF_BASE_SHA}";
-      fi;
-    - cd core
-    - corepack enable
-    - yarn install
-    - git diff ${CI_MERGE_REQUEST_TARGET_BRANCH_SHA:-$CI_MERGE_REQUEST_DIFF_BASE_SHA} --name-only 2>1 > /dev/null || (echo "Warning, cannot find changed files, converting to full clone." & (git fetch --unshallow --quiet && echo "Fetch successful."))
-    - git diff ${CI_MERGE_REQUEST_TARGET_BRANCH_SHA:-$CI_MERGE_REQUEST_DIFF_BASE_SHA} --name-only | sed "s_^_../_" | yarn run spellcheck:core --no-must-find-files --file-list stdin
+    - *cspell-cache
+    - *core-spellcheck
   cache:
     key:
       files:
diff --git a/core/.gitignore b/core/.gitignore
index 8b62a030998c85a90371b6f18d3aa9cddda9931e..f6b3bbea0fe3e4d80ee50347b770a946977d694b 100644
--- a/core/.gitignore
+++ b/core/.gitignore
@@ -26,3 +26,6 @@ nightwatch.settings.json
 
 # Ignore CSpell cache
 .cspellcache
+
+# Ignore phpstan cache
+phpstan-tmp
diff --git a/core/phpstan.neon.dist b/core/phpstan.neon.dist
index 35bcfd1e64b51a005a28a1a2ff8252241ba5ec98..f796fff520e29b2befdeebf1b6e594e37f5ced04 100644
--- a/core/phpstan.neon.dist
+++ b/core/phpstan.neon.dist
@@ -6,6 +6,8 @@ includes:
 
 parameters:
 
+  tmpDir: phpstan-tmp
+
   level: 1
 
   fileExtensions:
@@ -26,6 +28,8 @@ parameters:
     - ../*/node_modules/*
     - */tests/fixtures/*.php
     - */tests/fixtures/*.php.gz
+    # Skip the phpstan tmpDir
+    - phpstan-tmp/*
     # Skip Drupal's own PHPStan rules test fixtures.
     - tests/PHPStan/fixtures/*
     # Skip Drupal 6 & 7 code.