Welcome to the final chapter of the mobile project tutorial!
In the previous chapter, Chapter 4: Asynchronous Data Generation, we learned how to generate heavy data (QR codes) in the background so our UI appears instantly.
However, there is still one hidden performance issue. Even though the QR code generates in the background, the computer still has to read and load all the code for the mobile command just to start the CLI.
If your CLI has 100 different commands, loading all 100 files at startup would make the tool feel very slow.
In this chapter, we will implement Lazy Module Loading. We will ensure that the heavy code for our command is only loaded into memory when the user actually asks for it.
Imagine you are packing your backpack for school. You have 8 different classes, and each class has a heavy textbook.
For a CLI, "Lazy Loading" means we keep the initial startup lightweight.
When a user types:
claude --help
They should get a response immediately (in milliseconds). The CLI should simply read the list of names and descriptions. It should not load the heavy React libraries, QR generation logic, or networking code required for the mobile command yet.
The secret to Lazy Loading lies in how we define the command in our entry file, index.ts.
Usually, in TypeScript/JavaScript, we import code at the top of the file:
// β Eager Loading (Don't do this for commands)
// This loads the entire file immediately!
import { call } from './mobile.js';
const mobile = {
// ...
run: call
}
If we do this, the moment the CLI starts, it has to read mobile.js, which imports React, which imports Ink, which imports the QR library... this chain reaction slows everything down.
load Function
Instead, we use a special property in our command definition called load.
Let's look at index.ts:
import type { Command } from '../../commands.js'
const mobile = {
type: 'local-jsx',
name: 'mobile',
// ... aliases and description ...
// β
The Lazy Way
load: () => import('./mobile.js'),
} satisfies Command
Explanation:
() => import(...): This is a function. It is not executed immediately. It is just a set of instructions waiting to be run.import('./mobile.js'): This is a "Dynamic Import." It tells JavaScript: "Go find this file and load it into memory, but only when I tell you to."
The file index.ts remains tiny. It contains no logic, no React, and no heavy libraries. It is just a signpost.
export default mobile
Because index.ts is so small, the main CLI can read it almost instantly to build the help menu.
How does the framework know when to run the function? Let's trace the lifecycle of a command.
Deep inside the CLI framework (the code that runs your command), there is a logic flow that looks something like this:
// Inside the Framework Core
async function runCommand(commandName: string) {
// 1. Find the definition in the list
const command = commands.find(c => c.name === commandName);
// 2. Trigger the Lazy Load
// The heavy file is read from the hard drive HERE
const module = await command.load();
// 3. Execute the logic
if (command.type === 'local-jsx') {
await module.call(uiHandler);
}
}
Explanation:
await command.load(): This is the magic moment. The program pauses for a millisecond to go fetch the actual code file (mobile.js). This happens after the user has already pressed Enter.mobile
Our mobile command depends on:
If we didn't use Lazy Loading, the user would pay the "cost" of loading these three libraries every time they used the CLI, even if they were just checking the version or logging in.
By using load: () => import(...), we ensure that the "cost" is only paid by people who actually want to see the QR code.
Congratulations! You have successfully built the mobile command from scratch.
Let's review our journey:
index.ts) so the CLI knows our command exists.mobile.tsx) using React components like <Box> and <Text>.You now have a professional-grade, interactive, performant CLI feature. You can apply these same patternsβDefinition, UI, Events, Async Data, and Lazy Loadingβto build any tool you can imagine!
Generated by Code IQ