Introduction: Why Changing the URL Without Reloading is a Superpower
\n\nImagine: you open a modern web service — say, an email client or an online document editor. You switch between folders, open emails, change settings — and the page does not reload. Meanwhile, the browser's address bar changes, the "Back" and "Forward" buttons work, and bookmarks save the desired state. How does this work? The magic of modifying the URL without reloading the page.
\n\nThis is not just a "gimmick" — it is the foundation of high-performance single-page applications (SPAs). Once upon a time, developers used hashes (#), but that was ugly and inconvenient. Today we have the History API — a powerful tool that allows you to change the URL instantly, without flickering, without losing state, without unnecessary HTTP requests.
\n\nIn this article, we will break down how pushState and replaceState work, how they affect performance, and how to avoid common pitfalls during optimization. Let's go!
Basics: The History API and Its Methods
\n\nThe HTML5 specification gave us the window.history interface. The two main methods for changing the URL without reloading are pushState and replaceState.
pushState — Adding a New Entry to the History
\n\npushState creates a new entry in the browser's history. The user can press "Back" and return to the previous URL.
// Syntax\nhistory.pushState(state, title, url);\n\n// Example\nhistory.pushState({ page: 1 }, 'Home', '/home');\n\n\nParameters:
\n- \n
- state — a data object associated with this entry. You can store any serializable data (e.g., filter state, element ID). \n
- title — the page title (most browsers ignore it, but it's better to pass an empty string or meaningful text). \n
- url — the new URL. It must be relative or absolute, but within the same origin (protocol + domain + port). \n
replaceState — Replacing the Current Entry
\n\nreplaceState modifies the current entry in the history without creating a new one. The user will not be able to go "back" to the previous URL — it will be replaced.
// Syntax\nhistory.replaceState(state, title, url);\n\n// Example — updating the URL when a filter changes\nfunction applyFilter(filterName) {\n history.replaceState({ filter: filterName }, '', `?filter=${filterName}`);\n updateUI(filterName);\n}\n\n\npopstate — Listening for History Changes
\n\nWhen the user presses "Back" or "Forward", the browser fires the popstate event. Important: pushState and replaceState do not trigger this event — it needs to be handled separately.
window.addEventListener('popstate', function(event) {\n if (event.state) {\n // Restore state based on data from event.state\n restorePageState(event.state);\n } else {\n // If state is empty — the user may have navigated outside our application\n loadDefaultState();\n }\n});\n\n\nPerformance Optimization: What's Important to Know
\n\nModifying the URL is fast. But there are nuances that can turn a superpower into a source of bugs.
\n\n1. Don't Call pushState Too Often
\n\nEach call to pushState adds an entry to the history. If you call it on every mouse movement or every character typed in a search — the history will bloat, and the user will have to press "Back" 100 times to exit the filter. Use replaceState for intermediate states, and pushState for logically complete actions (e.g., opening a new page).