This final episode unites the entire material: measuring performance with evidence, avoiding memory leaks, applying debounce and lazy loading patterns, and structuring production-ready code with security, testing, and structured releases. You complete the Learn JavaScript journey.

Congratulations — you've made it to the final episode. Twenty-one episodes built your understanding from basic syntax to modern tooling. Episode 22 closes the journey with two themes that define application quality in the real world: performance and readiness.
Performance means the application doesn't just function — it functions fast and stable: light to load, smooth to respond, and without leaking memory over time. Readiness means your code is secure, tested, and released in a structured way. Both are what separates a prototype from a production application. Hold one principle from the start: don't optimize without evidence — measure first, find the real problem, then optimize.
Before changing anything, measure. performance.now gives precise timing:
function prosesData(angka) {
let total = 0;
for (const nilai of angka) {
total += nilai * 2;
}
return total;
}
const mulai = performance.now();
const hasil = prosesData([1, 2, 3, 4, 5]);
const selesai = performance.now();
console.log(`Hasil: ${hasil}, durasi: ${selesai - mulai} ms`);The tools you already know from episode 19 become useful again: the Performance tab in DevTools records a session and shows where time is actually wasted. Look for long tasks that block the page, then inspect the functions or layouts that dominate. Measurement results determine priority — sometimes the "coolest" optimization has no impact on a real application at all.
Accessing and manipulating the DOM is far slower than in-memory operations. Collect changes and mount them at once:
const kontainer = document.querySelector("#daftar");
const fragmen = document.createDocumentFragment();
for (let i = 1; i <= 100; i++) {
const item = document.createElement("li");
item.textContent = `Item ${i}`;
fragmen.append(item);
}
kontainer.append(fragmen);document.createDocumentFragment() holds elements in memory first, then mounts them all at once. Without this, 100 additions mean 100 render triggers; with a fragment, the browser renders once. The same principle applies to reflow — the expensive layout recalculation: gather style changes into one batch, for example kotak.style.display = "none", then change the class, then show it again, so it's one batch rather than three separate reflows.
JavaScript manages memory with garbage collection — objects that are no longer reachable are cleaned up automatically. A memory leak happens when objects that should be dead remain reachable, so the GC doesn't dare clean them up. The most common causes in web applications:
Every addEventListener holds a reference to its handler. If an element is removed but the listener isn't, the reference stays alive:
function buatKartu() {
const tombol = document.querySelector("#tombol");
function handler() {
console.log("Diklik");
}
tombol.addEventListener("click", handler);
return function hapus() {
tombol.removeEventListener("click", handler);
};
}
const bersihkan = buatKartu();
bersihkan();tombol.removeEventListener("click", handler) removes the listener with the exact same handler — which is why the handler must be stored in a variable rather than written inline anonymously. The returned hapus function gives you control over when cleanup happens.
Expensive operations triggered frequently — search-as-you-type, scrolling, resizing — can overwhelm an application. Debounce delays execution until a pause in activity; throttle limits execution to at most once per interval:
function debounce(fungsi, jeda) {
let timer = null;
return function (...argumen) {
clearTimeout(timer);
timer = setTimeout(() => {
fungsi(...argumen);
}, jeda);
};
}
const cari = debounce((kata) => {
console.log("Mencari:", kata);
}, 300);Loading the entire application at once slows down the first visit. Code splitting breaks the bundle into parts loaded on demand:
async function muatModulBerat() {
const modul = await import("./modul-berat.js");
return modul.proses();
}
tombol.addEventListener("click", () => {
muatModulBerat().then(console.log);
});Production code must consider security. The two rules most frequently violated:
innerHTML — an XSS hole. Always use textContent or escape the HTML.HttpOnly cookie.function teksAman(teks) {
const div = document.createElement("div");
div.textContent = teks;
return div.innerHTML;
}div.textContent = teks makes the browser escape special characters, so <script> becomes safe plain text to display. This pattern is a simple trick for showing user input without opening a hole.
Production-ready code also means structured processes:
major.minor.patch — for breaking changes, features, and fixes.All the tools were already covered in episodes 20 and 21: automated lint and format, reproducible builds with a lockfile, and npm scripts CI can invoke. Combining them into a release pipeline is the final step toward production.
Episode 22 closed your journey: measuring performance with performance.now and the Performance tab, optimizing the DOM with batching, preventing memory leaks by removing listeners, applying debounce and lazy loading, and structuring production code that is secure, tested, and released systematically.
Key takeaways:
removeEventListener must use the exact same handler.This is the end of the Learn JavaScript series. From episode 0, which prepared your environment, to episode 22, which prepared production, you've built a complete foundation: syntax, data structures, functions, the DOM, asynchrony, tooling, and performance. The real journey begins now — apply everything you've learned in real projects, and keep writing code every day. Happy coding!