CLI Performance Optimization
Techniques to make Node.js CLI tools start faster and run efficiently, from lazy loading and require caching to startup profiling and output streaming.
CLI Performance Optimization
A CLI tool that takes 500ms to start feels sluggish. One that takes 50ms feels instant. The difference is not the work the tool does — it is how the tool loads itself. Most CLI performance problems come from loading modules you do not need, parsing configuration you could defer, and doing I/O synchronously when you could stream.
I have profiled and optimized CLI tools from 800ms startup to under 50ms. The techniques are predictable and the gains are real. This guide covers every optimization that matters, from the first require() call to the final byte of output.
Prerequisites
- Node.js installed (v14+)
- A CLI tool to optimize (or willingness to build one)
- Familiarity with
require()and module loading - Basic understanding of Node.js event loop and streams
Measuring Startup Time
Before optimizing, measure. Use process.hrtime.bigint() for precise timing:
#!/usr/bin/env node
// Instrument startup at the very first line
var startTime = process.hrtime.bigint();
var path = require("path");
var fs = require("fs");
// After all requires
var requireTime = process.hrtime.bigint();
// After argument parsing
// ... parse args ...
var parseTime = process.hrtime.bigint();
// After config loading
// ... load config ...
var configTime = process.hrtime.bigint();
function reportTiming() {
var total = Number(process.hrtime.bigint() - startTime) / 1e6;
var requires = Number(requireTime - startTime) / 1e6;
var parsing = Number(parseTime - requireTime) / 1e6;
var config = Number(configTime - parseTime) / 1e6;
console.error("\n[timing]");
console.error(" requires: " + requires.toFixed(1) + "ms");
console.error(" parsing: " + parsing.toFixed(1) + "ms");
console.error(" config: " + config.toFixed(1) + "ms");
console.error(" total: " + total.toFixed(1) + "ms");
}
if (process.env.CLI_TIMING) {
process.on("exit", reportTiming);
}
Run with timing enabled:
CLI_TIMING=1 mytool --version
Output:
1.2.0
[timing]
requires: 245.3ms
parsing: 12.1ms
config: 8.4ms
total: 268.2ms
Using Node's Built-In Profiling
# Generate a startup trace
node --cpu-prof --cpu-prof-interval=100 ./bin/mytool.js --version
# This creates a .cpuprofile file
# Open it in Chrome DevTools: chrome://inspect -> Open dedicated DevTools
# Or use the --prof flag for V8 profiling
node --prof ./bin/mytool.js --version
node --prof-process isolate-0x*.log > profile.txt
Tracking require() Costs
Identify which modules are expensive to load:
// require-trace.js - Monkey-patch require to measure load times
var Module = require("module");
var originalLoad = Module._load;
var loadTimes = [];
Module._load = function(request, parent) {
var start = process.hrtime.bigint();
var result = originalLoad.apply(this, arguments);
var duration = Number(process.hrtime.bigint() - start) / 1e6;
if (duration > 1) { // Only track modules taking > 1ms
loadTimes.push({
module: request,
time: duration.toFixed(1),
from: parent ? parent.filename : "root"
});
}
return result;
};
process.on("exit", function() {
loadTimes.sort(function(a, b) { return parseFloat(b.time) - parseFloat(a.time); });
console.error("\n[require trace] Top 10 slowest modules:");
for (var i = 0; i < Math.min(10, loadTimes.length); i++) {
var entry = loadTimes[i];
console.error(" " + entry.time.padStart(8) + "ms " + entry.module);
}
});
Usage:
node -r ./require-trace.js ./bin/mytool.js --version
Output:
[require trace] Top 10 slowest modules:
85.2ms chalk
42.3ms yargs
38.1ms js-yaml
22.4ms glob
18.7ms ajv
12.1ms commander
8.3ms fs-extra
6.2ms inquirer
4.1ms ora
3.8ms cosmiconfig
Lazy Loading Modules
The single biggest optimization for CLI startup is lazy loading. Only require modules when the command that needs them actually runs.
Before: Everything Loaded Upfront
#!/usr/bin/env node
// BAD: All these load even for --version
var commander = require("commander");
var chalk = require("chalk"); // 85ms
var inquirer = require("inquirer"); // 120ms
var glob = require("glob"); // 22ms
var yaml = require("js-yaml"); // 38ms
var axios = require("axios"); // 45ms
var ora = require("ora"); // 30ms
var program = new commander.Command();
program.version("1.0.0");
program
.command("deploy")
.action(function() {
// Only deploy uses inquirer, axios, ora
// But they loaded for every command
});
program
.command("init")
.action(function() {
// Only init uses glob, yaml, inquirer
});
program.parse();
Startup time for --version: ~350ms (loading 6 unnecessary modules).
After: Lazy Loading Per Command
#!/usr/bin/env node
var commander = require("commander"); // 12ms - always needed
var program = new commander.Command();
program.version("1.0.0");
program
.command("deploy")
.action(function() {
// Load only when deploy runs
var inquirer = require("inquirer");
var axios = require("axios");
var ora = require("ora");
// ...
});
program
.command("init")
.action(function() {
var glob = require("glob");
var yaml = require("js-yaml");
var inquirer = require("inquirer");
// ...
});
program.parse();
Startup time for --version: ~18ms. Startup time for deploy: ~180ms (only loads what deploy needs).
Lazy Require Helper
function lazy(moduleName) {
var cached = null;
return function() {
if (!cached) {
cached = require(moduleName);
}
return cached;
};
}
// Define lazy loaders at the top
var getChalk = lazy("chalk");
var getInquirer = lazy("inquirer");
var getAxios = lazy("axios");
// Use in commands
function deploy() {
var chalk = getChalk();
var axios = getAxios();
console.log(chalk.green("Deploying..."));
}
Lazy Property Pattern
For modules with many exports where you only use one function:
function lazyProp(moduleName, property) {
var cached = null;
return function() {
if (!cached) {
var mod = require(moduleName);
cached = property ? mod[property] : mod;
}
return cached;
};
}
var getGlob = lazyProp("glob", "sync");
var getYamlParse = lazyProp("js-yaml", "load");
// Usage - only loads when called
var files = getGlob()("src/**/*.js");
var config = getYamlParse()(yamlContent);
Avoiding Heavy Dependencies
Some npm packages are disproportionately expensive to load. Replace them with lighter alternatives or built-in modules.
Common Replacements
// HEAVY: chalk (85ms to load)
// LIGHT: manual ANSI codes (0ms)
var colors = {
green: function(s) { return "\u001b[32m" + s + "\u001b[0m"; },
red: function(s) { return "\u001b[31m" + s + "\u001b[0m"; },
yellow: function(s) { return "\u001b[33m" + s + "\u001b[0m"; },
cyan: function(s) { return "\u001b[36m" + s + "\u001b[0m"; },
bold: function(s) { return "\u001b[1m" + s + "\u001b[0m"; },
dim: function(s) { return "\u001b[90m" + s + "\u001b[0m"; }
};
// Disable colors when not a TTY or NO_COLOR is set
if (!process.stdout.isTTY || process.env.NO_COLOR) {
var keys = Object.keys(colors);
for (var i = 0; i < keys.length; i++) {
colors[keys[i]] = function(s) { return s; };
}
}
// HEAVY: fs-extra (8ms)
// LIGHT: built-in fs with recursive option (0ms)
var fs = require("fs");
// fs-extra's mkdirp is now built-in:
fs.mkdirSync(dir, { recursive: true });
// fs-extra's readJson:
var data = JSON.parse(fs.readFileSync(file, "utf8"));
// fs-extra's copy:
fs.cpSync(src, dest, { recursive: true }); // Node 16.7+
// HEAVY: glob (22ms)
// LIGHT: fs.globSync (Node 22+) or manual walk
function walkSync(dir, pattern) {
var results = [];
var ext = pattern.split(".").pop();
function walk(currentDir) {
var entries = fs.readdirSync(currentDir, { withFileTypes: true });
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
var fullPath = path.join(currentDir, entry.name);
if (entry.isDirectory()) {
walk(fullPath);
} else if (entry.name.endsWith("." + ext)) {
results.push(fullPath);
}
}
}
walk(dir);
return results;
}
// HEAVY: js-yaml (38ms)
// If you only need simple YAML, parse it yourself or use JSON config instead
// HEAVY: axios (45ms)
// LIGHT: built-in http/https (0ms)
var https = require("https");
function httpGet(url) {
return new Promise(function(resolve, reject) {
https.get(url, function(res) {
var body = "";
res.on("data", function(chunk) { body += chunk; });
res.on("end", function() {
resolve({ status: res.statusCode, data: JSON.parse(body) });
});
}).on("error", reject);
});
}
Dependency Audit Script
#!/usr/bin/env node
// audit-deps.js - Measure the cost of each dependency
var childProcess = require("child_process");
var pkg = require("./package.json");
var deps = Object.keys(pkg.dependencies || {});
console.log("\nDependency load times:\n");
var results = [];
for (var i = 0; i < deps.length; i++) {
var dep = deps[i];
try {
var output = childProcess.execSync(
"node -e \"var s = process.hrtime.bigint(); require('" + dep + "'); " +
"var e = process.hrtime.bigint(); console.log(Number(e-s)/1e6)\"",
{ encoding: "utf8", timeout: 5000 }
).trim();
var time = parseFloat(output);
results.push({ name: dep, time: time });
} catch (e) {
results.push({ name: dep, time: -1 });
}
}
results.sort(function(a, b) { return b.time - a.time; });
for (var j = 0; j < results.length; j++) {
var r = results[j];
var bar = "";
var blocks = Math.floor(r.time / 5);
for (var k = 0; k < Math.min(blocks, 40); k++) bar += "█";
var timeStr = r.time >= 0 ? r.time.toFixed(1) + "ms" : "error";
var color = r.time > 50 ? "\u001b[31m" : r.time > 20 ? "\u001b[33m" : "\u001b[32m";
console.log(
" " + r.name.padEnd(25) +
color + timeStr.padStart(8) + "\u001b[0m " +
"\u001b[90m" + bar + "\u001b[0m"
);
}
var total = results.reduce(function(sum, r) { return sum + Math.max(0, r.time); }, 0);
console.log("\n Total: " + total.toFixed(1) + "ms\n");
Output:
Dependency load times:
inquirer 122.4ms ████████████████████████
axios 45.2ms █████████
chalk 85.1ms █████████████████
js-yaml 38.3ms ███████
glob 22.1ms ████
commander 12.3ms ██
minimist 2.1ms
Total: 327.5ms
Caching Expensive Operations
Cache results that do not change between invocations:
var fs = require("fs");
var path = require("path");
var os = require("os");
var crypto = require("crypto");
var CACHE_DIR = path.join(os.homedir(), ".mytool", "cache");
function ensureCacheDir() {
if (!fs.existsSync(CACHE_DIR)) {
fs.mkdirSync(CACHE_DIR, { recursive: true });
}
}
function getCacheKey(input) {
return crypto.createHash("md5").update(input).digest("hex");
}
function cacheGet(key, maxAgeMs) {
maxAgeMs = maxAgeMs || 3600000; // 1 hour default
var cachePath = path.join(CACHE_DIR, key + ".json");
try {
var stat = fs.statSync(cachePath);
var age = Date.now() - stat.mtimeMs;
if (age > maxAgeMs) {
return null; // Expired
}
return JSON.parse(fs.readFileSync(cachePath, "utf8"));
} catch (e) {
return null;
}
}
function cacheSet(key, value) {
ensureCacheDir();
var cachePath = path.join(CACHE_DIR, key + ".json");
fs.writeFileSync(cachePath, JSON.stringify(value));
}
// Usage: Cache config file parsing
function loadConfig(configPath) {
var content = fs.readFileSync(configPath, "utf8");
var cacheKey = getCacheKey(configPath + ":" + content);
var cached = cacheGet(cacheKey);
if (cached) {
return cached;
}
// Expensive: parse, validate, merge with defaults
var config = parseAndValidateConfig(content);
cacheSet(cacheKey, config);
return config;
}
// Usage: Cache API responses
function getRemoteConfig(url) {
var cacheKey = getCacheKey("remote:" + url);
var cached = cacheGet(cacheKey, 300000); // 5 min cache
if (cached) {
return Promise.resolve(cached);
}
return httpGet(url).then(function(response) {
cacheSet(cacheKey, response.data);
return response.data;
});
}
Streaming Output
For commands that produce large output, stream it instead of buffering:
var fs = require("fs");
var readline = require("readline");
// BAD: Buffer entire file, then process, then output
function searchFileBad(filePath, pattern) {
var content = fs.readFileSync(filePath, "utf8");
var lines = content.split("\n");
var matches = [];
for (var i = 0; i < lines.length; i++) {
if (lines[i].indexOf(pattern) !== -1) {
matches.push((i + 1) + ": " + lines[i]);
}
}
console.log(matches.join("\n"));
}
// GOOD: Stream the file and output matches as found
function searchFileGood(filePath, pattern) {
return new Promise(function(resolve) {
var lineNum = 0;
var matchCount = 0;
var rl = readline.createInterface({
input: fs.createReadStream(filePath),
crlfDelay: Infinity
});
rl.on("line", function(line) {
lineNum++;
if (line.indexOf(pattern) !== -1) {
matchCount++;
process.stdout.write(lineNum + ": " + line + "\n");
}
});
rl.on("close", function() {
resolve(matchCount);
});
});
}
Streaming JSON Output
For large JSON arrays, stream items instead of building the array in memory:
function streamJsonArray(items) {
process.stdout.write("[\n");
for (var i = 0; i < items.length; i++) {
var json = JSON.stringify(items[i], null, 2)
.split("\n")
.map(function(line) { return " " + line; })
.join("\n");
process.stdout.write(json);
if (i < items.length - 1) {
process.stdout.write(",");
}
process.stdout.write("\n");
}
process.stdout.write("]\n");
}
Parallel I/O Operations
When a command needs multiple independent resources, fetch them in parallel:
// BAD: Sequential fetches
function deploySequential(config) {
return checkHealth(config.healthUrl)
.then(function(health) {
return getVersion(config.apiUrl);
})
.then(function(version) {
return getDeployStatus(config.statusUrl);
})
.then(function(status) {
// All three fetched sequentially: ~900ms total
});
}
// GOOD: Parallel fetches
function deployParallel(config) {
return Promise.all([
checkHealth(config.healthUrl),
getVersion(config.apiUrl),
getDeployStatus(config.statusUrl)
]).then(function(results) {
var health = results[0];
var version = results[1];
var status = results[2];
// All three fetched in parallel: ~300ms total
});
}
// Parallel file operations
function processFiles(filePaths) {
var CONCURRENCY = 10;
var results = [];
var index = 0;
function next() {
if (index >= filePaths.length) return Promise.resolve();
var filePath = filePaths[index++];
return processFile(filePath).then(function(result) {
results.push(result);
return next();
});
}
var workers = [];
for (var i = 0; i < Math.min(CONCURRENCY, filePaths.length); i++) {
workers.push(next());
}
return Promise.all(workers).then(function() {
return results;
});
}
V8 Snapshot and Compile Cache
For maximum startup performance, use V8's compile cache:
// Enable compile cache (Node.js 22+)
// Set in the entry point before any other requires:
if (process.config && process.versions.node >= "22") {
var v8 = require("v8");
var path = require("path");
var os = require("os");
var cacheDir = path.join(os.homedir(), ".mytool", "v8-cache");
v8.enableCompileCache(cacheDir);
}
// For older Node versions, use --compiled-cache flag:
// node --compiled-cache=.cache ./bin/mytool.js
Complete Working Example: Optimized CLI
#!/usr/bin/env node
var startTime = process.hrtime.bigint();
// Only load the minimum needed for argument routing
var fs = require("fs");
var path = require("path");
// ANSI colors without dependencies
var c = {
green: function(s) { return "\u001b[32m" + s + "\u001b[0m"; },
red: function(s) { return "\u001b[31m" + s + "\u001b[0m"; },
yellow: function(s) { return "\u001b[33m" + s + "\u001b[0m"; },
cyan: function(s) { return "\u001b[36m" + s + "\u001b[0m"; },
bold: function(s) { return "\u001b[1m" + s + "\u001b[0m"; },
dim: function(s) { return "\u001b[90m" + s + "\u001b[0m"; }
};
if (!process.stdout.isTTY || process.env.NO_COLOR) {
var noColor = function(s) { return s; };
Object.keys(c).forEach(function(k) { c[k] = noColor; });
}
// Ultra-fast argument parsing (no library needed for simple CLIs)
function parseArgs(argv) {
var args = argv.slice(2);
var command = null;
var subcommand = null;
var flags = {};
var positional = [];
for (var i = 0; i < args.length; i++) {
var arg = args[i];
if (arg === "--version" || arg === "-V") { flags.version = true; continue; }
if (arg === "--help" || arg === "-h") { flags.help = true; continue; }
if (arg === "--verbose" || arg === "-v") { flags.verbose = true; continue; }
if (arg === "--timing") { flags.timing = true; continue; }
if (arg.indexOf("--") === 0) {
var eq = arg.indexOf("=");
if (eq !== -1) {
flags[arg.slice(2, eq)] = arg.slice(eq + 1);
} else if (i + 1 < args.length && args[i + 1].charAt(0) !== "-") {
flags[arg.slice(2)] = args[++i];
} else {
flags[arg.slice(2)] = true;
}
continue;
}
if (arg.charAt(0) === "-") {
flags[arg.slice(1)] = true;
continue;
}
if (!command) command = arg;
else if (!subcommand) subcommand = arg;
else positional.push(arg);
}
return { command: command, subcommand: subcommand, flags: flags, positional: positional };
}
var parsed = parseArgs(process.argv);
// Handle --version immediately (no loading needed)
if (parsed.flags.version) {
var pkg = require(path.join(__dirname, "..", "package.json"));
console.log(pkg.version);
process.exit(0);
}
// Handle --help
if (parsed.flags.help || !parsed.command) {
console.log("");
console.log(" " + c.bold("mytool") + " - Optimized CLI example");
console.log("");
console.log(" " + c.bold("Commands:"));
console.log(" search <pattern> Search files for a pattern");
console.log(" stats Show project statistics");
console.log(" deploy <target> Deploy the application");
console.log(" config show Show configuration");
console.log("");
console.log(" " + c.bold("Options:"));
console.log(" --version, -V Show version");
console.log(" --help, -h Show help");
console.log(" --verbose, -v Verbose output");
console.log(" --timing Show timing information");
console.log("");
process.exit(0);
}
// Timing setup
var timing = parsed.flags.timing || !!process.env.CLI_TIMING;
function reportTime(label) {
if (!timing) return;
var elapsed = Number(process.hrtime.bigint() - startTime) / 1e6;
process.stderr.write(c.dim("[" + elapsed.toFixed(1) + "ms] " + label) + "\n");
}
reportTime("args parsed");
// Route to command handlers (lazy-loaded)
switch (parsed.command) {
case "search":
reportTime("loading search");
var pattern = parsed.subcommand;
if (!pattern) {
console.error(c.red("Error:") + " Missing search pattern");
process.exit(1);
}
// Stream search - no heavy deps needed
var dir = parsed.flags.dir || process.cwd();
var matchCount = 0;
var fileCount = 0;
function searchDir(dirPath) {
var entries = fs.readdirSync(dirPath, { withFileTypes: true });
for (var i = 0; i < entries.length; i++) {
var entry = entries[i];
if (entry.name.charAt(0) === "." || entry.name === "node_modules") continue;
var fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
searchDir(fullPath);
continue;
}
if (!entry.name.match(/\.(js|ts|json|md|yaml|yml)$/)) continue;
try {
var content = fs.readFileSync(fullPath, "utf8");
var lines = content.split("\n");
for (var j = 0; j < lines.length; j++) {
if (lines[j].indexOf(pattern) !== -1) {
if (matchCount === 0) console.log("");
var relPath = path.relative(process.cwd(), fullPath);
console.log(
c.cyan(relPath) + ":" + c.yellow(String(j + 1)) +
": " + lines[j].trim()
);
matchCount++;
}
}
fileCount++;
} catch (e) {
// Skip binary files
}
}
}
searchDir(dir);
console.log("");
console.log(c.dim(matchCount + " matches in " + fileCount + " files"));
reportTime("search complete");
break;
case "stats":
reportTime("loading stats");
// Compute stats without external deps
var statsDir = process.cwd();
var stats = { files: 0, lines: 0, bytes: 0, types: {} };
function statDir(d) {
var entries = fs.readdirSync(d, { withFileTypes: true });
for (var i = 0; i < entries.length; i++) {
if (entries[i].name.charAt(0) === "." || entries[i].name === "node_modules") continue;
var fp = path.join(d, entries[i].name);
if (entries[i].isDirectory()) {
statDir(fp);
} else {
stats.files++;
var ext = path.extname(entries[i].name) || "(none)";
stats.types[ext] = (stats.types[ext] || 0) + 1;
try {
var st = fs.statSync(fp);
stats.bytes += st.size;
var content = fs.readFileSync(fp, "utf8");
stats.lines += content.split("\n").length;
} catch (e) {}
}
}
}
statDir(statsDir);
console.log("");
console.log(" " + c.bold("Project Statistics"));
console.log("");
console.log(" Files: " + stats.files);
console.log(" Lines: " + stats.lines.toLocaleString());
console.log(" Size: " + (stats.bytes / 1024).toFixed(0) + " KB");
console.log("");
console.log(" " + c.bold("By type:"));
var types = Object.keys(stats.types).sort(function(a, b) {
return stats.types[b] - stats.types[a];
});
for (var t = 0; t < Math.min(types.length, 10); t++) {
console.log(" " + types[t].padEnd(10) + " " + stats.types[types[t]]);
}
console.log("");
reportTime("stats complete");
break;
case "deploy":
reportTime("loading deploy module");
// Lazy-load heavy deps only for deploy
var target = parsed.subcommand || "staging";
console.log(c.bold("Deploying to " + target + "..."));
// Heavy modules loaded here, only when deploy runs
reportTime("deploy complete");
break;
default:
console.error(c.red("Unknown command: ") + parsed.command);
console.error("Run " + c.cyan("mytool --help") + " for usage");
process.exit(1);
}
if (timing) {
var totalTime = Number(process.hrtime.bigint() - startTime) / 1e6;
process.stderr.write(c.dim("\n[total] " + totalTime.toFixed(1) + "ms\n"));
}
Run with timing:
$ mytool --version --timing
1.0.0
[2.1ms] version printed
$ mytool search "require" --timing
[1.8ms] args parsed
[1.9ms] loading search
src/cli.js:3: var fs = require("fs");
src/cli.js:4: var path = require("path");
src/commands/deploy.js:1: var http = require("http");
3 matches in 12 files
[45.2ms] search complete
[total] 45.3ms
Common Issues and Troubleshooting
--version takes 200ms+ to respond
Your entry point loads heavy modules before checking for --version:
Fix: Check --version before any require() calls. Parse it manually from process.argv:
if (process.argv.indexOf("--version") !== -1) {
console.log(require("./package.json").version);
process.exit(0);
}
// Now load the heavy framework
Startup is fast locally but slow in CI
CI runners often have cold npm caches and slower disks. The V8 compile cache is also cold:
Fix: Enable V8 compile cache in CI. Consider pre-building your CLI with esbuild or ncc to bundle everything into a single file.
Memory usage grows with large output
Building a huge array and then JSON.stringify-ing it uses 2x the memory:
Fix: Stream output. Write items as they are processed instead of buffering them.
Lazy loading breaks require in transpiled code
Bundlers like webpack inline require() calls. Dynamic requires inside functions may not be resolved:
Fix: Use dynamic require() with string variables that bundlers cannot statically analyze, or configure your bundler to leave specific requires untouched.
Cache files cause stale behavior
Users update config but the CLI shows old values from cache:
Fix: Include a hash of the source data in the cache key. When the source changes, the cache key changes and the old cache is invalidated.
Best Practices
- Measure before optimizing. Add
--timingsupport to identify where time is actually spent. Intuition about bottlenecks is usually wrong. - Lazy-load everything that is not needed for argument routing. The only modules that must load at startup are your argument parser and the minimum for command dispatch.
- Replace heavy dependencies with built-in modules.
chalkcan be replaced with 10 lines of ANSI codes.axioscan be replaced with built-inhttps.fs-extrais mostly unnecessary since Node.js 16. - Handle
--versionand--helpbefore loading anything. These are the most common invocations and should return in under 20ms. - Cache expensive computations between runs. Config parsing, API responses, and file system scans can all be cached with TTL-based invalidation.
- Stream output for large results. Never buffer thousands of items just to print them. Write items as they are generated.
- Profile with
--cpu-profperiodically. New dependencies or features may introduce regressions. Catch them early. - Bundle for production. Tools like
esbuildorncccompile your entire CLI into a single file, eliminating require() resolution overhead entirely.