Faster startup: Lazy imports and V8 compile cache#7054
Faster startup: Lazy imports and V8 compile cache#7054gonzaloriestra wants to merge 1 commit intofaster-startup-3from
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
39c47da to
377c2e4
Compare
35d5a30 to
ea5104e
Compare
377c2e4 to
29e09f4
Compare
29e09f4 to
e1916a4
Compare
ea5104e to
7682630
Compare
Coverage report
Test suite run success4000 tests passing in 1531 suites. Report generated by 🧪jest coverage report action from 0b07426 |
e1916a4 to
8909fbd
Compare
1c3373e to
efa9dd4
Compare
8909fbd to
222f528
Compare
efa9dd4 to
40f806b
Compare
222f528 to
0b07426
Compare
40f806b to
df491cd
Compare
0b07426 to
5f36d08
Compare
5f36d08 to
cf295bb
Compare
c8a2bca to
e1532d8
Compare
7e62b50 to
01576da
Compare
e1532d8 to
13679d3
Compare
e90935c to
dd0a4d5
Compare
|
/snapit |
This comment was marked as outdated.
This comment was marked as outdated.
dd0a4d5 to
8870349
Compare
3c23d93 to
44f4592
Compare
8dfbb9c to
14a02dd
Compare
|
/snapit |
This comment was marked as outdated.
This comment was marked as outdated.
14a02dd to
3d49ec6
Compare
44f4592 to
1bc4fb3
Compare
|
/snapit |
This comment was marked as outdated.
This comment was marked as outdated.
1bc4fb3 to
5255928
Compare
|
/snapit |
This comment was marked as outdated.
This comment was marked as outdated.
3d49ec6 to
f2a02c6
Compare
5255928 to
257f8e0
Compare
|
/snapit |
f2a02c6 to
5a19697
Compare
3a05243 to
a53f20d
Compare
|
/snapit |
|
🫰✨ Thanks @gonzaloriestra! Your snapshot has been published to npm. Test the snapshot by installing your package globally: npm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20260413124802Caution After installing, validate the version by running |
5a19697 to
4bffb74
Compare
a53f20d to
942d8b4
Compare
Defer heavy module imports to point of use across the codebase: - base-command: lazy ui.js (600KB React/Ink), error-handler, notifications - prerun hook: parallel imports via Promise.all - postrun hook: lazy analytics and deprecations - node-package-manager: lazy latest-version import - context/local: lazy fs.js and system.js (break circular deps) - is-global: lazy output.js, system.js, version.js, ui.js - custom-oclif-loader: fire-and-forget init hooks via runHook override Enable V8 compile cache via module.enableCompileCache() in dev.js/run.js, caching bytecode between runs to eliminate ~30ms compile overhead. Combined improvement: ~130ms across all commands Made-with: Cursor
4bffb74 to
a4c95cc
Compare
942d8b4 to
6a9a3b0
Compare
Differences in type declarationsWe detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:
New type declarationspackages/cli-kit/dist/public/node/custom-oclif-loader.d.tsimport { Command, Config } from '@oclif/core';
/**
* Optional lazy command loader function.
* If set, ShopifyConfig will use it to load individual commands on demand
* instead of importing the entire COMMANDS module (which triggers loading all packages).
*/
export type LazyCommandLoader = (id: string) => Promise<typeof Command | undefined>;
/**
* Subclass of oclif's Config that loads command classes on demand for faster CLI startup.
*/
export declare class ShopifyConfig extends Config {
private lazyCommandLoader?;
/**
* Set a lazy command loader that will be used to load individual command classes on demand,
* bypassing the default oclif behavior of importing the entire COMMANDS module.
*
* @param loader - The lazy command loader function.
*/
setLazyCommandLoader(loader: LazyCommandLoader): void;
/**
* Override runCommand to use lazy loading when available.
* Instead of calling cmd.load() which triggers loading ALL commands via index.js,
* we directly import only the needed command module.
*
* @param id - The command ID to run.
* @param argv - The arguments to pass to the command.
* @param cachedCommand - An optional cached command loadable.
* @returns The command result.
*/
runCommand<T = unknown>(id: string, argv?: string[], cachedCommand?: Command.Loadable | null): Promise<T>;
}
Existing type declarationspackages/cli-kit/dist/public/node/cli-launcher.d.ts@@ -1,6 +1,8 @@
+import type { LazyCommandLoader } from './custom-oclif-loader.js';
interface Options {
moduleURL: string;
argv?: string[];
+ lazyCommandLoader?: LazyCommandLoader;
}
/**
* Launches the CLI.
packages/cli-kit/dist/public/node/cli.d.ts@@ -1,3 +1,4 @@
+import type { LazyCommandLoader } from './custom-oclif-loader.js';
/**
* IMPORTANT NOTE: Imports in this module are dynamic to ensure that "setupEnvironmentVariables" can dynamically
* set the DEBUG environment variable before the 'debug' package sets up its configuration when modules
@@ -7,6 +8,8 @@ interface RunCLIOptions {
/** The value of import.meta.url of the CLI executable module */
moduleURL: string;
development: boolean;
+ /** Optional lazy command loader for on-demand command loading */
+ lazyCommandLoader?: LazyCommandLoader;
}
/**
* A function that abstracts away setting up the environment and running
@@ -17,6 +20,7 @@ export declare function runCLI(options: RunCLIOptions & {
runInCreateMode?: boolean;
}, launchCLI?: (options: {
moduleURL: string;
+ lazyCommandLoader?: LazyCommandLoader;
}) => Promise<void>, argv?: string[], env?: NodeJS.ProcessEnv, versions?: NodeJS.ProcessVersions): Promise<void>;
/**
* A function for create-x CLIs that automatically runs the "init" command.
@@ -38,5 +42,5 @@ export declare const jsonFlag: {
/**
* Clear the CLI cache, used to store some API responses and handle notifications status
*/
-export declare function clearCache(): void;
+export declare function clearCache(): Promise<void>;
export {};
\ No newline at end of file
packages/cli-kit/dist/public/node/error.d.ts@@ -1,6 +1,6 @@
-import { AlertCustomSection } from './ui.js';
import { OutputMessage } from './output.js';
import { InlineToken, TokenItem } from '../../private/node/ui/components/TokenizedText.js';
+import type { AlertCustomSection } from './ui.js';
export { ExtendableError } from 'ts-error';
export declare enum FatalErrorType {
Abort = 0,
packages/cli-kit/dist/public/node/is-global.d.ts@@ -1,4 +1,4 @@
-import { PackageManager } from './node-package-manager.js';
+import type { PackageManager } from './node-package-manager.js';
/**
* Returns true if the current process is running in a global context.
*
packages/cli-kit/dist/public/node/notifications-system.d.ts@@ -24,7 +24,7 @@ declare const NotificationSchema: zod.ZodObject<{
surface: zod.ZodOptional<zod.ZodString>;
}, "strip", zod.ZodTypeAny, {
id: string;
- type: "error" | "info" | "warning";
+ type: "info" | "error" | "warning";
message: string;
frequency: "always" | "once" | "once_a_day" | "once_a_week";
ownerChannel: string;
@@ -41,7 +41,7 @@ declare const NotificationSchema: zod.ZodObject<{
surface?: string | undefined;
}, {
id: string;
- type: "error" | "info" | "warning";
+ type: "info" | "error" | "warning";
message: string;
frequency: "always" | "once" | "once_a_day" | "once_a_week";
ownerChannel: string;
@@ -84,7 +84,7 @@ declare const NotificationsSchema: zod.ZodObject<{
surface: zod.ZodOptional<zod.ZodString>;
}, "strip", zod.ZodTypeAny, {
id: string;
- type: "error" | "info" | "warning";
+ type: "info" | "error" | "warning";
message: string;
frequency: "always" | "once" | "once_a_day" | "once_a_week";
ownerChannel: string;
@@ -101,7 +101,7 @@ declare const NotificationsSchema: zod.ZodObject<{
surface?: string | undefined;
}, {
id: string;
- type: "error" | "info" | "warning";
+ type: "info" | "error" | "warning";
message: string;
frequency: "always" | "once" | "once_a_day" | "once_a_week";
ownerChannel: string;
@@ -120,7 +120,7 @@ declare const NotificationsSchema: zod.ZodObject<{
}, "strip", zod.ZodTypeAny, {
notifications: {
id: string;
- type: "error" | "info" | "warning";
+ type: "info" | "error" | "warning";
message: string;
frequency: "always" | "once" | "once_a_day" | "once_a_week";
ownerChannel: string;
@@ -139,7 +139,7 @@ declare const NotificationsSchema: zod.ZodObject<{
}, {
notifications: {
id: string;
- type: "error" | "info" | "warning";
+ type: "info" | "error" | "warning";
message: string;
frequency: "always" | "once" | "once_a_day" | "once_a_week";
ownerChannel: string;
packages/cli-kit/dist/public/node/hooks/postrun.d.ts@@ -1,3 +1,7 @@
+/**
+ * Postrun hook — uses dynamic imports to avoid loading heavy modules (base-command, analytics)
+ * at module evaluation time. These are only needed after the command has already finished.
+ */
import { Hook } from '@oclif/core';
/**
* Check if post run hook has completed.
packages/cli-kit/dist/public/node/hooks/prerun.d.ts@@ -14,4 +14,4 @@ export declare function parseCommandContent(cmdInfo: {
* Triggers a background check for a newer CLI version (non-blocking).
* The result is cached and consumed by the postrun hook for auto-upgrade.
*/
-export declare function checkForNewVersionInBackground(): void;
\ No newline at end of file
+export declare function checkForNewVersionInBackground(): Promise<void>;
\ No newline at end of file
|
|
/snapit |
|
🫰✨ Thanks @gonzaloriestra! Your snapshot has been published to npm. Test the snapshot by installing your package globally: npm i -g --@shopify:registry=https://registry.npmjs.org @shopify/cli@0.0.0-snapshot-20260414093626Caution After installing, validate the version by running |
|
Started reviewing this stack today. Spent some time today exploring if the approaches in this PR are idiomatic and if there are more idiomatic approaches with similar performance profiles. Not sure I'll wrap that up today. Commenting to let you know I am looking into this. |

WHY are these changes introduced?
The CLI requires too much time to start working, it feels slow
WHAT is this pull request doing?
Defers heavy module imports to point of use across the codebase and enables V8 compile caching for faster subsequent runs.
ui.js(600KB React/Ink),error-handler.js,notifications-system.js,environments.js— only loaded when actually neededPromise.allfor analytics, notifications, and package-manager moduleslatest-versionimport (~113ms saved)fs.jsandsystem.jsto break circular dependency chainsoutput.js,system.js,version.js,ui.jsmodule.enableCompileCache()indev.js/run.jscaches bytecode between runs, eliminating ~30ms compile overheadCombined improvement of the stack: 500-700ms faster for any command, and the startup is ~3x faster.
shopify-version.mp4
How to test your changes?
pnpm i -g --@shopify:registry=https://registry.npmjs.org @0.0.0-snapshot-20260414093626Measuring impact
Checklist