You've just diagnosed a production performance issue. loop-detective told you that processPayload at /app/handlers/payment.js:127 consumed 54% of CPU, that three HTTP calls to the payment gateway averaged 1.8 seconds, and that the event loop lagged 12 times during the 30-second capture.
Now you need to share this with your team. You copy-paste the terminal output into Slack. The ANSI color codes turn into garbage. The bar charts become meaningless characters. The carefully formatted report becomes an unreadable wall of text.
This is why we built HTML report output for node-loop-detective v2.0.0.
loop-detective 12345 --html report.html
One flag. One file. Open it in any browser. Share it anywhere.
The Problem With Terminal Output
Terminal output is perfect for the person running the diagnostic. It's immediate, it's colorful, it's right there. But it has three fundamental limitations:
It doesn't travel well. ANSI escape codes render as
[31min Slack, email, Jira, and most text editors. You lose all formatting.It's not interactive. You can't collapse a call stack you don't care about. You can't hover over a lag event to see its timestamp. You can't sort the function table by percentage.
It's ephemeral. Once the terminal scrolls past, it's gone. You can redirect to a file, but then you have a plain text file with escape codes.
JSON output (--json) solves the data portability problem but creates a readability problem. Nobody wants to read a 500-line JSON blob in a Slack thread.
What the HTML Report Looks Like
The report is a single self-contained HTML file with a dark theme inspired by GitHub's UI. No external CSS, no JavaScript CDN links, no images to load. It works offline, it works on any device, and it's about 15-20KB for a typical report.
It has six sections:
Summary Cards
Five cards at the top showing the key numbers at a glance: profiling duration, CPU sample count, number of hot functions, lag event count, and slow I/O operation count. You can tell in one second whether this report is interesting.
Diagnosis
Each detected pattern gets a colored severity badge (HIGH in red, MED in yellow, LOW in green), a description, the code location, and a suggested fix. This is the same information as the terminal output, but formatted for readability.
CPU-Heavy Functions Table
A sortable table with visual percentage bars. The bars are color-coded: red for >50% CPU, yellow for >20%, green for the rest. Each row shows the function name, a visual bar, self time in milliseconds, and the file location.
Collapsible Call Stacks
The top blocking functions have expandable call stacks. Click to expand, click again to collapse. This keeps the report compact by default while making the full detail available on demand. The target function is highlighted in yellow.
Slow I/O Summary
Grouped by type (HTTP, FETCH, DNS, TCP), then by target. Each group shows call count, total duration, average, and max. Caller stack traces are shown inline. Error counts get a red badge.
Event Loop Lag Timeline
A visual timeline of lag events represented as colored dots. The dot size scales with lag severity. Hover over any dot to see the exact lag duration and timestamp. Below the timeline, a table aggregates lag events by code location.
Design Decisions
Why self-contained?
The report must work when attached to a Jira ticket, emailed to a colleague, or opened six months later during a post-mortem. External dependencies (CDN-hosted CSS, JavaScript libraries, web fonts) break in all these scenarios. Corporate firewalls block CDNs. Offline access fails. Links rot.
Every byte of CSS and JavaScript is inline in the HTML file. You can put it on a USB drive and it works.
Why dark theme?
Developers spend their days looking at dark-themed editors and terminals. A bright white report is jarring. The GitHub-inspired dark theme (#0d1117 background, #c9d1d9 text) feels native to the developer workflow.
Why collapsible call stacks?
A report with 5 expanded call stacks, each 10-15 frames deep, is overwhelming. Most of the time you only care about one or two stacks. Collapsible sections let you scan the targets quickly and expand only what's relevant.
The implementation is vanilla JavaScript — no framework, no build step:
document.querySelectorAll('.collapsible').forEach(el => {
el.addEventListener('click', () => {
el.classList.toggle('open');
el.nextElementSibling.classList.toggle('show');
});
});
Why not a full dashboard?
We considered generating a report with charts (line graphs for lag over time, pie charts for CPU distribution). But charts require a charting library (Chart.js, D3, etc.), which would either bloat the file or require a CDN. The current approach — colored bars, sized dots, tables — conveys the same information with pure HTML and CSS.
If you need charts, use --json and feed the data to your preferred visualization tool.
How It Works
The HTML report generator (src/html-report.js) is a pure function that takes analysis data and returns an HTML string:
function generateHtmlReport(data) {
const { analysis, lagEvents, slowIOEvents, config, timestamp } = data;
return `<!DOCTYPE html>
<html>
<head>
<style>/* all CSS inline */</style>
</head>
<body>
${renderSummary(analysis.summary, lagEvents, slowIOEvents)}
${renderPatterns(analysis.blockingPatterns)}
${renderHeavyFunctions(analysis.heavyFunctions)}
${renderCallStacks(analysis.callStacks)}
${renderSlowIO(slowIOEvents)}
${renderLagEvents(lagEvents)}
<script>/* collapsible toggle */</script>
</body>
</html>`;
}
Each section is rendered by a dedicated function. All user-provided strings (function names, file paths, error messages) are HTML-escaped to prevent XSS — even though the report is generated locally, it's good practice.
In the CLI, the events are captured before the reporter clears them:
detective.on('profile', (analysis, rawProfile) => {
// Capture before onProfile clears the arrays
const capturedLags = [...reporter.lagEvents];
const capturedIO = [...reporter.slowIOEvents];
reporter.onProfile(analysis); // prints to terminal + clears arrays
if (config.html) {
const html = generateHtmlReport({
analysis, lagEvents: capturedLags, slowIOEvents: capturedIO,
config, timestamp: analysis.timestamp,
});
fs.writeFileSync(path.resolve(config.html), html);
}
});
Combining Output Formats
The HTML report works alongside all other output options:
# Terminal output + HTML report
loop-detective 12345 --html report.html
# Terminal + HTML + CPU profile for flame graphs
loop-detective 12345 -d 60 --html report.html --save-profile profile.cpuprofile
# JSON + HTML (JSON goes to stdout, HTML to file)
loop-detective 12345 --json --html report.html > data.json
Each format serves a different purpose:
- Terminal: immediate diagnosis while you're at the keyboard
- HTML: sharing with team, attaching to tickets, archiving
- JSON: feeding to monitoring systems, custom analysis scripts
-
.cpuprofile: deep visual analysis in Chrome DevTools or speedscope
Programmatic API
The HTML generator is exported for use in custom tooling:
const { generateHtmlReport } = require('node-loop-detective');
// Generate from your own data
const html = generateHtmlReport({
analysis: myAnalysisResult,
lagEvents: myLagEvents,
slowIOEvents: mySlowIOEvents,
config: { duration: 30000, threshold: 50 },
timestamp: Date.now(),
});
// Serve it from an Express endpoint
app.get('/debug/report', (req, res) => {
res.type('html').send(html);
});
// Or save to S3 for the incident timeline
await s3.putObject({
Bucket: 'incident-reports',
Key: `diagnostics/${Date.now()}.html`,
Body: html,
ContentType: 'text/html',
});
Real-World Usage
Incident Post-Mortems
During an incident, run loop-detective with --html:
loop-detective 12345 -d 60 --html incident-2025-03-15.html
Attach the HTML file to the post-mortem document. Six months later, anyone can open it and see exactly what was happening: which functions were hot, which I/O was slow, when the lag events occurred.
Code Review Evidence
Found a performance regression? Profile the old and new versions, generate HTML reports for both, and attach them to the pull request. The reviewer can open both files side by side and compare.
Automated Reporting
In a CI/CD pipeline or scheduled job:
loop-detective --port 9229 -d 30 --html /reports/daily-$(date +%Y%m%d).html --json > /reports/daily-$(date +%Y%m%d).json
Build a simple file server over the reports directory and you have a performance history dashboard.
Try It
npm install -g node-loop-detective@2.0.0
loop-detective <pid> --html report.html
Open report.html in your browser. Share it with your team. Attach it to the ticket. The diagnostic data is no longer trapped in your terminal.

Top comments (0)