While working on a project recently, I needed to clean up some minified JavaScript that was basically unreadable. The code worked fine, but trying to debug it was like reading hieroglyphics without the Rosetta Stone. I opened my browser's dev tools, copied the code, and started searching for a quick way to format it.
The first thing I found was a bunch of online tools. Some were great, but they all had the same problem: I'd paste my code into a random website and hope they weren't logging my snippets. For work projects, that's a non-starter. For personal projects, it's still uncomfortable.
I wanted a simple solution without relying on external services. Something that runs entirely in the browser, works offline, and doesn't send my code anywhere. Because apparently I enjoy reinventing wheels.
The "Just Use a Library" Approach
Before building anything, I considered my options. Writing a full HTML/JS/CSS parser from scratch? Absolutely not. That's a rabbit hole of edge cases that would consume weeks.
The pragmatic choice was js-beautify, a battle-tested library that handles all three languages. It's the same engine behind many popular online formatters, so I knew it handled edge cases I'd never think about.
The harder question was minification. js-beautify handles formatting, but for minification I had options:
- Terser - Excellent for JS, but doesn't handle HTML or CSS
- Clean-css - Great for CSS, but nothing else
- html-minifier-terser - Handles HTML but pulls in a lot of dependencies
Each library would need its own integration and error handling. That's three different APIs to learn and maintain.
The Trade-off That Sounded Wrong but Worked
Here's where I made a decision that initially felt wrong: I used js-beautify for both beautification AND minification.
Wait, what? js-beautify doesn't minify. You're right. But here's the thing — for a browser-based tool, the minification doesn't need to be production-grade. It needs to:
- Remove comments
- Collapse whitespace
- Remove unnecessary newlines
- Keep the code valid
For most use cases — cleaning up copied snippets, reducing file size for quick sharing, making code more compact — that's enough. If someone needs aggressive minification with dead-code elimination, they should use a proper build tool anyway.
The trade-off was clear: js-beautify gives me one consistent API for all three languages, and I can control the output through its options. The minification won't beat Terser's compression ratio, but it's reliable and simple.
Setting Up the Core Logic
The heart of the tool is surprisingly small. Here's the essence:
function beautifyCode() {
const code = document.getElementById('input').value;
const lang = document.getElementById('lang').value;
const indent = document.getElementById('indent').value;
const opts = { indent_size: indent === '\t' ? 1 : parseInt(indent), indent_char: indent === '\t' ? '\t' : ' ' };
try {
let result;
if (lang === 'html') result = html_beautify(code, opts);
else if (lang === 'js') result = js_beautify(code, opts);
else result = css_beautify(code, opts);
document.getElementById('output').value = result;
updateStats(code, result);
} catch (e) {
showStatus('error', 'Failed to format code. Check for syntax errors.');
}
}
For minification, I configure the same library with different options:
function minifyCode() {
const code = document.getElementById('input').value;
const lang = document.getElementById('lang').value;
const opts = { indent_size: 1, indent_char: ' ', preserve_newlines: false, max_preserve_newlines: 0, wrap_line_length: 0 };
if (lang === 'js') {
// Remove comments before minifying
opts.jslint_happy = true;
opts.space_after_anon_function = true;
}
try {
let result;
if (lang === 'html') result = html_beautify(code, { ...opts, indent_inner_html: true });
else if (lang === 'js') result = js_beautify(code, opts);
else result = css_beautify(code, { ...opts, selector_separator_newline: false });
document.getElementById('output').value = result;
updateStats(code, result);
} catch (e) {
showStatus('error', 'Failed to minify code.');
}
}
The trick is realizing that minification and beautification are the same operation with different settings. Set preserve_newlines to false and wrap_line_length to 0, and the beautifier produces compact output. Not perfect minification, but solid enough for a browser tool.
The CDN Problem I Didn't Expect
The first version loaded js-beautify from jsdelivr. It worked perfectly in my testing. Then I deployed it and waited for feedback.
A few days later, I got a message from a user in China: "The tool doesn't work. The button does nothing."
I opened the console and saw it immediately: ReferenceError: html_beautify is not defined.
The CDN was blocked. jsdelivr is frequently blocked or slow in mainland China, and my user couldn't load the library at all. The page rendered fine, but every button silently failed.
This is the kind of bug that's easy to miss if you're only testing from one region. The fix was two-fold:
- Switch to a CDN that's more reliable in China (
staticfile.org) - Add a fallback that shows a clear error message if the library fails to load
<script src="https://cdn.staticfile.org/js-beautify/1.15.1/beautifier.min.js"
onerror="window.__beautifierFailed=true"></script>
Then in the click handlers:
function checkLib() {
if (window.__beautifierFailed || typeof html_beautify === 'undefined') {
showStatus('error', 'Code formatter library failed to load. Check your internet connection.');
return false;
}
return true;
}
Spoiler: it was never a CSS issue this time. It was a CDN availability issue. Classic "works on my machine" situation.
Handling the Edge Cases
With the core logic working, I started testing edge cases. Here's what broke:
Empty input: Clicking "Beautify" with nothing in the textarea should do nothing, not throw an error.
Invalid syntax: Paste in broken code and js-beautify sometimes throws, sometimes produces garbage. I needed to catch errors and show a friendly message.
Very large files: A 2MB minified file can freeze the browser. I added a size check and a warning.
Special characters: The library handles these fine, but my statistics calculation needed to use byte counts, not character counts. Chinese characters in comments or strings would throw off the size calculation.
The statistics display was a nice touch that users appreciated:
function updateStats(input, output) {
const inBytes = new Blob([input]).size;
const outBytes = new Blob([output]).size;
const ratio = inBytes > 0 ? Math.round((1 - outBytes / inBytes) * 100) : 0;
document.getElementById('stats').textContent =
`Original: ${formatSize(inBytes)} → Result: ${formatSize(outBytes)} (${ratio}%)`;
}
new Blob([input]).size gives me the exact byte count, which handles Unicode correctly.
Building This With AI Assistance
I used AI assistance throughout this project, and it was a mixed bag. Here's the honest breakdown.
What AI handled well:
- The initial structure and CSS styling. I described the layout I wanted, and it generated a clean, responsive design with dark mode support in minutes.
- The i18n setup. I asked for a lightweight translation system, and it produced a clean
t()function withdata-i18nattributes. - The statistics calculation. It got the
Blobapproach right on the first try.
What AI got wrong:
The first version of the minify function was a mess. Here's what it generated:
// AI's first attempt (wrong)
function minifyCode() {
const code = document.getElementById('input').value;
// ... setup ...
const result = code.replace(/\s+/g, ' ').trim();
// This breaks strings, regexes, and template literals!
}
This naive whitespace removal would destroy JavaScript. It would break template literals, regex patterns, and string concatenation. I had to explain that minification requires understanding the language structure, not just regex replacement.
The AI also initially ignored the CDN fallback issue entirely. I had to explicitly ask for error handling around the library loading.
Where I had to step in:
- The CDN fallback logic (AI didn't consider regional availability)
- The error handling for invalid input (AI assumed valid input)
- The performance considerations for large files (AI didn't think about it)
- The minification strategy (AI suggested using regex, which was wrong)
What I Learned
1. Browser-only tools have a real place. For quick formatting tasks, sending code to a server is overkill and a privacy concern. A client-side tool is instant, private, and works offline.
2. "Good enough" is often the right call. My minification isn't as aggressive as a dedicated minifier, but it handles 95% of use cases with one library instead of three.
3. CDN choice matters more than you think. If you're building a global tool, test from multiple regions. A library that works in the US might be completely inaccessible elsewhere.
4. AI is a great pair programmer, not a replacement. It handled the boilerplate and styling perfectly. But it didn't understand the domain-specific pitfalls of code manipulation. I had to know enough to catch its mistakes.
The Result
During this process, I built a small browser-based tool to make this workflow easier. It's a single HTML file that handles HTML, JavaScript, and CSS formatting in both directions — minify and beautify — with customizable indentation, file statistics, and a clean interface. Everything runs locally in your browser, so your code never leaves your machine.
If you're dealing with unreadable minified code or want to compress your CSS before deployment, it might save you a few minutes. You can find it at craftvo.app.
The next time you're staring at a wall of minified JavaScript, remember: you don't need to send your code to a random server just to make it readable. A few lines of JavaScript and a good library can do it right in your browser. And if you're building something similar, don't forget to test your CDN from more than one continent.
Top comments (0)