HTML Interview Questions

20+ HTML interview questions and answers in quiz-style format, answered by ex-FAANG interviewers
Questions and solutions by ex-interviewers
Covers critical topics

HTML interview questions are designed to assess your understanding of web development fundamentals and best practices. Interviewers typically focus on key topics such as:

  • Accessibility: Ensuring websites are accessible to users with disabilities using semantic HTML and ARIA roles.
  • Semantics: Recognizing the importance of semantic HTML tags for SEO, accessibility, and code clarity.
  • Forms: Building forms with proper input validation, accessibility features, and efficient handling of form submissions.
  • Multimedia: Embedding and managing images, audio, and video in HTML while optimizing for performance and accessibility.
  • Best Practices: Structuring HTML for readability, maintainability, and performance, including the proper use of meta tags, link attributes, and media queries.
  • SEO Optimization: Using semantic HTML elements and metadata to boost search engine ranking and improve web performance.

Below, you’ll find 20+ carefully curated HTML interview questions covering everything from core concepts to best practices and optimization strategies.

Each question includes:

  • Quick Answers (TL;DR): Concise, clear responses to help you answer confidently.
  • Detailed Explanations: In-depth insights to ensure you not only know the answers but understand the reasoning behind them.

Unlike most lists, our questions are carefully curated by real senior and staff engineers from top tech companies like Amazon, Meta, and more—not anonymous contributors or AI-generated content. Start practicing below and get ready to ace your HTML interview!

If you're looking for HTML coding questions -We've got you covered as well, with:
Javascript coding
  • 70+ HTML coding interview questions
  • An in-browser coding workspace that mimics real interview conditions
  • Reference solutions from ex-interviewers at Big Tech companies
  • One-click automated, transparent test cases
  • Instant UI preview for UI-related questions
Get Started
Join 50,000+ engineers

Describe the difference between `<script>`, `<script async>` and `<script defer>`

Topics
HTMLJavaScript

TL;DR

All of these ways (<script>, <script async>, and <script defer>) are used to load and execute JavaScript files in an HTML document, but they differ in how the browser handles loading and execution of the script:

  • <script> is the default way of including JavaScript. The browser blocks HTML parsing while the script is being downloaded and executed. The browser will not continue rendering the page until the script has finished executing.
  • <script async> downloads the script asynchronously, in parallel with parsing the HTML. Executes the script as soon as it is available, potentially interrupting the HTML parsing. Multiple <script async> tags do not wait for each other and execute in no particular order.
  • <script defer> downloads the script asynchronously, in parallel with parsing the HTML. However, the execution of the script is deferred until HTML parsing is complete, in the order they appear in the HTML.

Here's a table summarizing the 4 ways of loading <script>s in an HTML document. Modern apps almost always use modules, which deserve their own row.

Feature<script><script async><script defer><script type="module">
Parsing behaviorBlocks HTML parsingDownloads in parallel; execution still blocks parsingDownloads in parallel; execution deferred until after parsingDownloads in parallel; execution deferred until after parsing
Execution orderIn order of appearanceNot guaranteedIn order of appearanceIn order of appearance, with each script's import dependencies resolved first
DOM dependencyNoNoYes (waits for DOM)Yes (waits for DOM)

What <script> tags are for

<script> tags are used to include JavaScript on a web page. The async and defer attributes are used to change how/when the loading and execution of the script happens.

<script>

For normal <script> tags without any async or defer, when they are encountered, HTML parsing is blocked, the script is fetched and executed immediately. HTML parsing resumes after the script is executed. This can block rendering of the page if the script is large.

Use <script> for critical scripts that the page relies on to render properly.

<!doctype html>
<html>
<head>
<title>Regular Script</title>
</head>
<body>
<!-- Content before the script -->
<h1>Regular Script Example</h1>
<p>This content will be rendered before the script executes.</p>
<!-- Regular script -->
<script src="regular.js"></script>
<!-- Content after the script -->
<p>This content will be rendered after the script executes.</p>
</body>
</html>

<script async>

In <script async>, the browser downloads the script file asynchronously (in parallel with HTML parsing) and executes it as soon as it is available (potentially before HTML parsing completes). The execution will not necessarily be executed in the order in which it appears in the HTML document. This can improve perceived performance because the browser doesn't wait for the script to download before continuing to render the page.

Use <script async> when the script is independent of any other scripts on the page, for example, analytics and ads scripts.

<!doctype html>
<html>
<head>
<title>Async Script</title>
</head>
<body>
<!-- Content before the script -->
<h1>Async Script Example</h1>
<p>This content will be rendered before the async script executes.</p>
<!-- Async script -->
<script async src="async.js"></script>
<!-- Content after the script -->
<p>
This content may be rendered before or after the async script executes.
</p>
</body>
</html>

<script defer>

Similar to <script async>, <script defer> also downloads the script in parallel to HTML parsing, but the script is only executed when the document has been fully parsed and before firing DOMContentLoaded. If there are multiple of them, each deferred script is executed in the order they appear in the HTML document.

If a script relies on a fully-parsed DOM, the defer attribute will be useful in ensuring that the HTML is fully parsed before executing.

<!doctype html>
<html>
<head>
<title>Deferred Script</title>
</head>
<body>
<!-- Content before the script -->
<h1>Deferred Script Example</h1>
<p>This content will be rendered before the deferred script executes.</p>
<!-- Deferred script -->
<script defer src="deferred.js"></script>
<!-- Content after the script -->
<p>This content will be rendered before the deferred script executes.</p>
</body>
</html>

<script type="module">

Module scripts are the standard entry point for projects built with Vite (and many other modern bundlers). Next.js doesn't always emit type="module" for its own runtime, but most application code authored as ES modules ends up running through one of these tags. They behave like defer scripts with two important additions: dependencies declared with import are loaded and executed in the right order, and module code is strict by default.

<script type="module" src="/src/main.js"></script>

Behavior:

  • Parsing: deferred. HTML parsing continues; the script does not block.
  • Execution: runs after the document has finished parsing. Across multiple <script type="module"> tags in the same document, execution follows document order, but each script's import dependencies are resolved and executed first.
  • DOM ready: the DOM is parsed before the module runs.
  • Strict mode: enforced automatically; no 'use strict' directive needed.
  • CORS: module scripts are always fetched with CORS, so the server must send Access-Control-Allow-Origin for cross-origin loads. The crossorigin attribute itself is not required for the module to load — it only controls whether credentials (cookies, HTTP auth) are sent on cross-origin requests (crossorigin or crossorigin="anonymous" omits them; crossorigin="use-credentials" sends them). Adding it is still recommended for explicit credentials handling and for full error details in error event handlers.

Use <script type="module"> for new front-end code. If you have an independent module-script entry point and document order does not matter, combine with async:

<script async type="module" src="/analytics-module.js"></script>

Which to use: a decision matrix

Script typeUse for
<script> (no attrs)Critical inline scripts that must run synchronously before the next HTML element parses.
<script async>Independent third-party scripts where order does not matter (analytics, ads, monitoring beacons).
<script defer>Classic (non-module) app scripts where order matters and the DOM should be ready.
<script type="module">ES module entry points. The default for Vite, Next.js client code, and any new project.
<script async type="module">Module scripts where order does not matter. Most apps want default module behavior instead.

How modern frameworks load scripts

It also helps to know what the tools you use actually generate.

  • Vite: emits <script type="module" crossorigin src="..."> for the entry chunk in production. Dynamic import() calls become <link rel="modulepreload"> hints plus lazy module fetches.
  • Next.js: provides a built-in <Script> component with strategy props such as beforeInteractive (loaded and executed before page hydration), afterInteractive (default, like defer), lazyOnload (after the page is idle), and worker (offloaded to Partytown).
  • CDN-injected scripts (Google Analytics, Plausible, Sentry, etc.) are almost always recommended as async, because they are independent. Loading them with defer or no attribute slows down the page for no reason.
  • CRA and older webpack apps typically emit <script defer src="..."></script> for the runtime and chunk entries. CRA itself is deprecated, and new projects should use Vite or Next.js.

Common bugs from the wrong attribute choice

  • Analytics with defer instead of async. defer waits for HTML parsing, so a third-party tag at the top of the page artificially extends DOMContentLoaded. Use async for any independent third-party tag.
  • App entry as async script. If app.js and vendor.js are loaded with async, they can execute in any order. app.js may run before vendor.js finishes, which throws ReferenceError for the missing globals. Use defer (or modules) for app scripts.
  • document.write inside a defer or async script. Browsers ignore document.write() calls from async or deferred scripts with a console warning: "A call to document.write() from an asynchronously-loaded external script was ignored." The script would need to be a regular blocking <script> for the call to take effect, though document.write should be avoided in any new code.
  • Module script in a <script> tag without type="module". Top-level import statements throw SyntaxError. Either set type="module" or use a bundler.
  • Cross-origin module served without CORS headers. Module scripts are always fetched with CORS, so a cross-origin URL like <script type="module" src="https://cdn.example.com/lib.js"> will fail to load if the server doesn't send Access-Control-Allow-Origin. The fix is on the server side — adding crossorigin to the tag does not bypass this requirement (though it is still recommended for better error reporting and explicit credentials handling).

Notes

  • The async attribute should be used for scripts that are not critical to the initial rendering of the page and do not depend on each other, while the defer attribute should be used for scripts that depend on or are depended on by another script.
  • The async and defer attributes are ignored for inline scripts (scripts with no src attribute).
  • <script>s with defer or async that contain document.write() will be ignored with a message like "A call to document.write() from an asynchronously-loaded external script was ignored".
  • Even though async and defer help to make script downloading asynchronous, the scripts are still eventually executed on the main thread. If these scripts are computationally intensive, it can result in laggy/frozen UI. Partytown is a library that helps relocate script executions into a web worker and off the main thread, which is great for third-party scripts where you do not have control over the code.

Further reading

What is the difference between `mouseenter` and `mouseover` event in JavaScript and browsers?

Topics
Web APIsHTMLJavaScript

TL;DR

The main difference lies in the bubbling behavior of mouseenter and mouseover events. mouseenter does not bubble while mouseover bubbles.

mouseenter events do not bubble. The mouseenter event is triggered only when the mouse pointer enters the element itself, not its descendants. If a parent element has child elements, and the mouse pointer enters child elements, the mouseenter event will not be triggered on the parent element again; it is only triggered once upon entry of the parent element, without regard for its contents. If both parent and child have mouseenter listeners attached and the mouse pointer moves from the parent element to the child element, mouseenter will only fire for the child.

mouseover events bubble up the DOM tree. The mouseover event is triggered when the mouse pointer enters the element or one of its descendants. If a parent element has child elements, and the mouse pointer enters child elements, the mouseover event will be triggered on the parent element again as well. If the parent element has multiple child elements, this can result in multiple event callbacks fired. If there are child elements, and the mouse pointer moves from the parent element to the child element, mouseover will fire for both the parent and the child.

Propertymouseentermouseover
BubblingNoYes
TriggerOnly when entering itselfWhen entering itself and when entering descendants

mouseenter event:

  • Does not bubble: The mouseenter event does not bubble. It is only triggered when the mouse pointer enters the element to which the event listener is attached, not when it enters any child elements.
  • Triggered once: The mouseenter event is triggered only once when the mouse pointer enters the element, making it more predictable and easier to manage in certain scenarios.

A use case for mouseenter is when you want to detect the mouse entering an element without worrying about child elements triggering the event multiple times.

mouseover Event:

  • Bubbles up the DOM: The mouseover event bubbles up through the DOM. This means that if you have an event listener on a parent element, it will also trigger when the mouse pointer moves over any child elements.
  • Triggered multiple times: The mouseover event is triggered every time the mouse pointer moves over an element or any of its child elements. This can lead to multiple triggers if you have nested elements.

A use case for mouseover is when you want to detect when the mouse enters an element or any of its children and are okay with the events triggering multiple times.

Example

Here's an example demonstrating the difference between mouseover and mouseenter events:

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mouse Events Example</title>
<style>
.parent {
width: 200px;
height: 200px;
background-color: lightblue;
padding: 20px;
}
.child {
width: 100px;
height: 100px;
background-color: lightcoral;
}
</style>
</head>
<body>
<div class="parent">
Parent Element
<div class="child">Child Element</div>
</div>
<script>
const parent = document.querySelector('.parent');
const child = document.querySelector('.child');
// Mouseover event on parent.
parent.addEventListener('mouseover', () => {
console.log('Mouseover on parent');
});
// Mouseenter event on parent.
parent.addEventListener('mouseenter', () => {
console.log('Mouseenter on parent');
});
// Mouseover event on child.
child.addEventListener('mouseover', () => {
console.log('Mouseover on child');
});
// Mouseenter event on child.
child.addEventListener('mouseenter', () => {
console.log('Mouseenter on child');
});
</script>
</body>
</html>

Expected behavior

  • When the mouse enters the parent element:
    • The mouseover event on the parent will trigger.
    • The mouseenter event on the parent will trigger.
  • When the mouse enters the child element:
    • The mouseover event on the parent will trigger again because mouseover bubbles up from the child.
    • The mouseover event on the child will trigger.
    • The mouseenter event on the child will trigger.
    • The mouseenter event on the parent will not trigger again because mouseenter does not bubble.

Further reading

Explain the difference between `document.querySelector()` and `document.getElementById()`

Topics
Web APIsJavaScriptHTML

TL;DR

document.querySelector() and document.getElementById() are both methods used to select elements from the DOM, but they have key differences. document.querySelector() can select any element using a CSS selector and returns the first match, while document.getElementById() selects an element by its ID and returns the element with that specific ID.

// Using document.querySelector()
const element = document.querySelector('.my-class');
// Using document.getElementById()
const elementById = document.getElementById('my-id');

Difference between document.querySelector() and document.getElementById()

document.querySelector()

  • Can select elements using any valid CSS selector, including class, ID, tag, attribute, and pseudo-classes
  • Returns the first element that matches the specified selector
  • More versatile but slightly slower due to the flexibility of CSS selectors
// Select the first element with the class 'my-class'
const element = document.querySelector('.my-class');
// Select the first <div> element
const divElement = document.querySelector('div');
// Select the first element with the attribute data-role='button'
const buttonElement = document.querySelector('[data-role="button"]');

document.getElementById()

  • Selects an element by its ID attribute
  • Returns the element with the specified ID
  • Faster and more efficient for selecting elements by ID, but less versatile
// Select the element with the ID 'my-id'
const elementById = document.getElementById('my-id');

Key differences

  • Selector type: document.querySelector() uses CSS selectors, while document.getElementById() uses only the ID attribute.
  • Return value: document.querySelector() returns the first matching element, whereas document.getElementById() returns the element with the specified ID.
  • Performance: document.getElementById() is generally faster because it directly accesses the element by ID, while document.querySelector() has to parse the CSS selector.

The full DOM-query method comparison

querySelector and getElementById are two of the seven main DOM-query methods. The most important practical distinction across all of them is whether the result is a live or a static collection.

MethodSelector inputReturnsLive?When to use
getElementById(id)id stringElement or nullNo (single element)Single element by id; hot-path code
querySelector(sel)Any CSS selectorFirst match or nullNo (snapshot)First element matching any selector
querySelectorAll(sel)Any CSS selectorStatic NodeListNo (snapshot)All matches as a frozen list
getElementsByClassName(name)Class nameHTMLCollectionYes (auto-updates)When you need a live collection
getElementsByTagName(tag)Tag nameHTMLCollectionYesSame
getElementsByName(name)name attributeNodeListYesForm elements by name
closest(sel)Any CSS selectorNearest matching ancestor (or self) or nullN/AWalking up from a target inside event handlers

The live vs snapshot distinction is a common source of subtle bugs.

Live vs static collections (predict the output)

document.body.innerHTML = '<div class="x"></div><div class="x"></div>';
const live = document.getElementsByClassName('x'); // HTMLCollection (live)
const snapshot = document.querySelectorAll('.x'); // NodeList (static)
console.log('before:', live.length, snapshot.length); // 2, 2
document.body.insertAdjacentHTML('beforeend', '<div class="x"></div>');
console.log('after: ', live.length, snapshot.length); // 3, 2

The live HTMLCollection updates automatically when DOM nodes are added or removed. The static NodeList from querySelectorAll does not.

This matters in practice in two specific ways:

  • Iterating a live collection while mutating it is a classic infinite-loop bug. Reading live[i] after appending more matching nodes hits the new ones too, and the for loop never finishes.
  • Caching a querySelectorAll result is safe; caching getElementsByClassName is not. The cached reference silently changes as the DOM changes.

Prefer querySelectorAll for most modern use cases (it has forEach, and array spread [...] works on it). Reach for the live collections only when you specifically want auto-updating behavior.

Performance: how much does it actually matter?

The "getElementById is faster" claim is technically true but practically irrelevant in nearly all code:

  • A single call to any of these methods on a typical page takes well under a microsecond on modern browsers. Engines maintain an internal id-to-element map for getElementById, and they typically optimize querySelector('#myId') to use the same fast path for the simple #id selector form.
  • The performance gap only matters inside hot loops doing tens of thousands of calls per frame, such as interactive canvases, grid renderers, and animation systems. In ordinary application code (event handlers, init code, framework hooks), choose by clarity, not micro-benchmarks.
  • For complex selectors (such as 'div.menu > a:nth-child(odd)'), querySelector parses the selector on every call (engines may cache internally, but it is not guaranteed by the spec). The cost is small per call and well under the 16ms budget for a 60Hz frame, but it does add up in tight loops — cache the result if you query the same selector repeatedly.

If you genuinely need maximum speed for repeated lookups, cache the reference:

const button = document.getElementById('action'); // cache once
button.addEventListener('click', handle); // reuse forever

The common waste is re-querying inside event handlers, not the choice between methods.

What about closest(), matches(), and contains()?

Three more methods round out the modern DOM-query toolkit:

  • element.closest(selector) walks up from the element (including the element itself) and returns the nearest ancestor matching the selector, or null. It is constantly useful in event-delegation handlers:

    table.addEventListener('click', (event) => {
    const row = event.target.closest('tr[data-id]');
    if (row) editRow(row.dataset.id);
    });
  • element.matches(selector) returns true or false for whether the element matches the CSS selector. Useful inside delegated handlers when you want to confirm the target type without a tagName check.

  • parent.contains(child) returns true if child is parent or anywhere inside it. Useful for outside-click detection: if (!modal.contains(event.target)) close().

These three plus querySelector and querySelectorAll form the modern toolkit. The older getElementsBy* methods are legacy and rarely the right default in new code.

Further reading

How do `<iframe>` on a page communicate?

Topics
Web APIsJavaScriptHTML

TL;DR

<iframe> elements on a page can communicate using the postMessage API. This allows for secure cross-origin communication between the parent page and the iframe. The postMessage method sends a message, and the message event listener receives it. Here's a simple example:

// In the parent page
const iframe = document.querySelector('iframe');
iframe.contentWindow.postMessage('Hello from parent', '*');
// In the iframe
window.addEventListener('message', (event) => {
console.log(event.data); // 'Hello from parent'
});

How do <iframe> on a page communicate?

Using the postMessage API

The postMessage API is the most common and secure way for iframes to communicate with each other or with their parent page. This method allows for cross-origin communication, which is essential for modern web applications.

Sending a message

To send a message from the parent page to the iframe, you can use the postMessage method. Here’s an example:

// In the parent page
const iframe = document.querySelector('iframe');
iframe.contentWindow.postMessage('Hello from parent', '*');

In this example, the parent page selects the iframe and sends a message to it. The second parameter, '*', is the target origin. It specifies the origin of the target window. Using '*' means the message can be received by any origin, but for security reasons, it's better to specify the exact origin.

Receiving a message

To receive a message in the iframe, you need to add an event listener for the message event:

// In the iframe
window.addEventListener('message', (event) => {
console.log(event.data); // 'Hello from parent'
});

The event object contains the data property, which holds the message sent by the parent page.

Security considerations

When using postMessage, it's crucial to consider security:

  • Specify the target origin: Instead of using '*', specify the exact origin to ensure that only messages from trusted sources are received.
  • Validate the message: Always validate the message content to prevent malicious data from being processed.

Example with target origin

Here’s an example with a specified target origin:

// In the parent page
const iframe = document.querySelector('iframe');
const targetOrigin = 'https://example.com';
iframe.contentWindow.postMessage('Hello from parent', targetOrigin);
// In the iframe
window.addEventListener('message', (event) => {
if (event.origin === 'https://parent.com') {
console.log(event.data); // 'Hello from parent'
}
});

In this example, the parent page sends a message only to https://example.com, and the iframe processes the message only if it comes from https://parent.com.

Further reading

How do you add, remove, and modify HTML elements using JavaScript?

Topics
Web APIsJavaScriptHTML

TL;DR

To add, remove, and modify HTML elements using JavaScript, you can use methods like createElement, appendChild, removeChild, and properties like innerHTML and textContent. For example, to add an element, you can create it using document.createElement and then append it to a parent element using appendChild. To remove an element, you can use removeChild on its parent. To modify an element, you can change its innerHTML or textContent.

// Adding an element
const newElement = document.createElement('div');
newElement.textContent = 'Hello, World!';
document.body.appendChild(newElement);
// Removing an element
const elementToRemove = document.getElementById('elementId');
elementToRemove.parentNode.removeChild(elementToRemove);
// Modifying an element
const elementToModify = document.getElementById('elementId');
elementToModify.innerHTML = 'New Content';

Adding, removing, and modifying HTML elements using JavaScript

Adding elements

To add an HTML element, you can use the document.createElement method to create a new element and then append it to a parent element using appendChild.

// Create a new div element
const newDiv = document.createElement('div');
// Set its content
newDiv.textContent = 'Hello, World!';
// Append the new element to the body
document.body.appendChild(newDiv);
// See the changed document by running the code
console.log(document.body);

You can also use insertBefore to insert the new element before an existing child element.

const parentElement = document.getElementById('parent');
const newElement = document.createElement('p');
newElement.textContent = 'Inserted Paragraph';
const referenceElement = document.getElementById('reference');
parentElement.insertBefore(newElement, referenceElement);

Removing elements

To remove an HTML element, you can use the removeChild method on its parent element.

// Select the element to be removed
const elementToRemove = document.getElementById('elementId');
// Remove the element
elementToRemove.parentNode.removeChild(elementToRemove);

Alternatively, you can use the remove method directly on the element.

const elementToRemove = document.getElementById('elementId');
elementToRemove.remove();

Modifying elements

To modify an HTML element, you can change its properties such as innerHTML, textContent, or attributes.

const elementToModify = document.createElement('div');
// Change its inner HTML
elementToModify.innerHTML = 'New Content';
// Change its text content
elementToModify.textContent = 'New Text Content';
// Change an attribute
elementToModify.setAttribute('class', 'new-class');
console.log(elementToModify);

You can also use methods like classList.add, classList.remove, and classList.toggle to modify the element's classes.

const element = document.getElementById('elementId');
// Add a class
element.classList.add('new-class');
// Remove a class
element.classList.remove('old-class');
// Toggle a class
element.classList.toggle('active');

Further reading

What is the difference between `event.preventDefault()` and `event.stopPropagation()`?

Topics
Web APIsHTMLJavaScript

TL;DR

event.preventDefault() is used to prevent the default action that belongs to the event, such as preventing a form from submitting. event.stopPropagation() is used to stop the event from bubbling up to parent elements, preventing any parent event handlers from being executed.


What is the difference between event.preventDefault() and event.stopPropagation()?

event.preventDefault()

event.preventDefault() is a method that cancels the event if it is cancelable, meaning that the default action that belongs to the event will not occur. For example, this can be used to prevent a form from being submitted:

document.querySelector('form').addEventListener('submit', function (event) {
event.preventDefault();
// Form submission is prevented
});

event.stopPropagation()

event.stopPropagation() is a method that prevents the event from bubbling up the DOM tree, stopping any parent handlers from being notified of the event. This is useful when you want to handle an event at a specific level and do not want it to trigger handlers on parent elements:

document.querySelector('.child').addEventListener('click', function (event) {
event.stopPropagation();
// Click event will not propagate to parent elements
});

Key differences

  • event.preventDefault() stops the default action associated with the event.
  • event.stopPropagation() stops the event from propagating (bubbling) up to parent elements.

Use cases

  • Use event.preventDefault() when you want to prevent the default behavior of an element, such as preventing a link from navigating or a form from submitting.
  • Use event.stopPropagation() when you want to prevent an event from reaching parent elements, which can be useful in complex UIs where multiple elements have event listeners.

Further reading

What is the difference between `innerHTML` and `textContent`?

Topics
Web APIsHTMLJavaScript

TL;DR

innerHTML and textContent are both properties used to get or set the content of an HTML element, but they serve different purposes. innerHTML returns or sets the HTML markup contained within the element, which means it can parse and render HTML tags. On the other hand, textContent returns or sets the text content of the element, ignoring any HTML tags and rendering them as plain text.

// Example of innerHTML
element.innerHTML = '<strong>Bold Text</strong>'; // Renders as bold text
// Example of textContent
element.textContent = '<strong>Bold Text</strong>'; // Renders as plain text: <strong>Bold Text</strong>

Difference between innerHTML and textContent

innerHTML

innerHTML is a property that allows you to get or set the HTML markup contained within an element. It can parse and render HTML tags, making it useful for dynamically updating the structure of a webpage.

Example
const element = document.getElementById('example');
element.innerHTML = '<strong>Bold Text</strong>'; // This will render as bold text
Use cases
  • Dynamically adding or updating HTML content
  • Rendering HTML tags and elements
Security considerations

Using innerHTML can expose your application to Cross-Site Scripting (XSS) attacks if you insert untrusted content. Always sanitize any user input before setting it as innerHTML.

textContent

textContent is a property that allows you to get or set the text content of an element. It ignores any HTML tags and renders them as plain text, making it safer for inserting user-generated content.

Example
const element = document.getElementById('example');
element.textContent = '<strong>Bold Text</strong>'; // This will render as plain text: <strong>Bold Text</strong>
Use cases
  • Safely inserting user-generated content
  • Stripping HTML tags from a string
Performance considerations

textContent is generally faster than innerHTML because it does not parse and render HTML tags. It simply updates the text content of the element.

Further reading

What is the DOM and how is it structured?

Topics
JavaScriptHTML

TL;DR

The DOM, or Document Object Model, is a programming interface for web documents. It represents the page so that programs can change the document structure, style, and content. The DOM is structured as a tree of objects, where each node represents part of the document, such as elements, attributes, and text.


What is the DOM and how is it structured?

Definition

The Document Object Model (DOM) is a cross-platform and language-independent interface that treats an HTML, XHTML, or XML document as a tree structure. Each node in this tree represents a part of the document.

Structure

The DOM is structured as a hierarchical tree of nodes. Here are the main types of nodes:

  1. Document node: The root of the document tree. It represents the entire document.
  2. Element nodes: These represent HTML elements and form the bulk of the document tree.
  3. Attribute nodes: These are associated with element nodes and represent the attributes of those elements.
  4. Text nodes: These represent the text content within elements.
  5. Comment nodes: These represent comments in the HTML.

Example

Consider the following HTML:

<!doctype html>
<html>
<head>
<title>Document</title>
</head>
<body>
<h1>Hello, World!</h1>
<p>This is a paragraph.</p>
</body>
</html>

The DOM tree for this document would look like this:

Document
└── html
├── head
│ └── title
│ └── "Document"
└── body
├── h1
│ └── "Hello, World!"
└── p
└── "This is a paragraph."

Accessing and manipulating the DOM

JavaScript can be used to access and manipulate the DOM. Here are some common methods:

  • document.getElementById(id): Selects an element by its ID.
  • document.querySelector(selector): Selects the first element that matches a CSS selector.
  • element.appendChild(node): Adds a new child node to an element.
  • element.removeChild(node): Removes a child node from an element.

Example:

// Create an <h1> element and add it to the DOM
const newElement = document.createElement('h1');
document.body.appendChild(newElement);
// Get the h1 element using querySelector
const heading = document.querySelector('h1');
heading.textContent = 'Hello, DOM!';
console.log(heading); // <h1>Hello, DOM!</h1>

Further reading

What's the difference between an "attribute" and a "property" in the DOM?

Topics
Web APIsJavaScriptHTML

TL;DR

Attributes are defined in the HTML and provide initial values for properties. Properties are part of the DOM and represent the current state of an element. For example, the value attribute of an <input> element sets its initial value, while the value property reflects the current value as the user interacts with it.


Difference between an "attribute" and a "property" in the DOM

Attributes

Attributes are defined in the HTML markup and provide initial values for elements. They are static and do not change once the page is loaded unless explicitly modified using JavaScript.

Example
<input type="text" value="initial value" />

In this example, value="initial value" is an attribute.

Properties

Properties are part of the DOM and represent the current state of an element. They are dynamic and can change as the user interacts with the page or through JavaScript.

Example
const inputElement = document.querySelector('input');
console.log(inputElement.value); // Logs the current value of the input element
inputElement.value = 'new value'; // Changes the current value of the input element

In this example, value is a property of the inputElement object.

Key differences

  • Initialization: Attributes initialize DOM properties.
  • State: Attributes are static, while properties are dynamic.
  • Access: Attributes can be accessed using getAttribute and setAttribute methods, while properties can be accessed directly on the DOM object.
Example
<input id="myInput" type="text" value="initial value" />
const inputElement = document.getElementById('myInput');
// Accessing attribute
console.log(inputElement.getAttribute('value')); // "initial value"
// Accessing property
console.log(inputElement.value); // "initial value"
// Changing property
inputElement.value = 'new value';
console.log(inputElement.value); // "new value"
console.log(inputElement.getAttribute('value')); // "initial value"

In this example, changing the value property does not affect the value attribute.

The classic example: input value (try it live)

This is the canonical interview demonstration. Open the input below, type into it, and watch how the attribute stays at the initial value while the property tracks what the user typed:

document.body.innerHTML = `
<input id="demo" type="text" value="initial value" />
<button id="check">Log attribute vs property</button>
<pre id="out"></pre>
`;
const input = document.getElementById('demo');
const out = document.getElementById('out');
document.getElementById('check').addEventListener('click', () => {
out.textContent =
`attribute (getAttribute('value')): ${input.getAttribute('value')}\n` +
`property (input.value): ${input.value}`;
});
// Programmatic demo if no user typing happens:
input.value = 'typed by user';
out.textContent =
`attribute (getAttribute('value')): ${input.getAttribute('value')}\n` +
`property (input.value): ${input.value}`;

The takeaway: the attribute is the original markup; the property is the current state. They diverge the moment the user (or your code) interacts with the element.

To reset an input back to its attribute, set the property back to the attribute value (input.value = input.defaultValue). To persist a new initial value across resets, you have to set the attribute too (input.setAttribute('value', '...') or input.defaultValue = '...').

Other attributes that diverge from their properties

The value attribute is the most-cited example, but several other attributes have a meaningfully different DOM property. Each of these comes up regularly in real bugs.

checked on checkboxes and radios

document.body.innerHTML = `<input type="checkbox" id="cb" checked />`;
const cb = document.getElementById('cb');
console.log(cb.getAttribute('checked')); // "" (present in markup)
console.log(cb.checked); // true
cb.checked = false; // user un-checks (or your code does)
console.log(cb.getAttribute('checked')); // still "" (attribute did not change)
console.log(cb.checked); // false

Same pattern: the attribute is the default state used on form reset; the property is the current state.

disabled on inputs and buttons

document.body.innerHTML = `<button id="b">Submit</button>`;
const b = document.getElementById('b');
b.setAttribute('disabled', 'disabled'); // sets the attribute
console.log(b.disabled); // true (property reflects the attribute)
b.disabled = false; // sets the property
console.log(b.getAttribute('disabled')); // null (attribute is removed)

Boolean attributes like disabled, readonly, required, and hidden are reflected: setting the property automatically adds or removes the attribute. This is different from value/checked, where the attribute stays put.

class (attribute) vs className and classList (properties)

document.body.innerHTML = `<div id="d" class="a b c"></div>`;
const d = document.getElementById('d');
console.log(d.getAttribute('class')); // "a b c"
console.log(d.className); // "a b c" (same string)
console.log(d.classList); // DOMTokenList ['a', 'b', 'c']
d.classList.add('d');
console.log(d.getAttribute('class')); // "a b c d"

The attribute is class; the DOM property is className (because class is a reserved word in JavaScript). The modern classList API gives you add/remove/toggle methods that update both for you.

for (attribute) vs htmlFor (property)

The for attribute on a <label> becomes the htmlFor property, for the same reason (for is a reserved word in JavaScript). This is a common stumbling block for React developers because JSX uses htmlFor:

// React JSX uses the property name
<label htmlFor="email">Email</label>;
// Plain HTML uses the attribute name
<label for="email">Email</label>;

style: string vs object

The style attribute is a string. The style property is a CSSStyleDeclaration object:

element.getAttribute('style'); // 'color: red; font-size: 14px' (string)
element.style; // CSSStyleDeclaration object; element.style.color === 'red'

You read individual rules from the property (element.style.fontSize) but cannot read computed values that way. For computed styles, use getComputedStyle(element).fontSize.

Why this matters in React, Vue, and other frameworks

Frameworks set properties in some places and attributes in others, and knowing which they choose explains a class of "this attribute won't update" bugs.

  • React sets DOM properties when a matching one exists, so <input value={x}> updates the property, not the attribute. This is why defaultValue exists: it is the way to set the underlying attribute. The same pattern applies to defaultChecked on checkboxes.
  • Vue 3 intelligently chooses between property and attribute for each binding. For standard interactive elements (such as value on <input>, <select>, and <progress>), it sets the DOM property. You can force one or the other with the .prop and .attr modifiers on v-bind.
  • Custom elements declared with attributes need attributeChangedCallback and observedAttributes to react to attribute changes. Properties do not go through that path.

If you ever see a React form where setting <input value={state}> works fine, but inspecting the rendered DOM shows the value="..." attribute stuck at the old value, that is not a bug. The attribute is intentionally the initial-value marker; the property is what reflects the live state.

Further reading

Difference between document `load` event and document `DOMContentLoaded` event?

Topics
HTMLJavaScript

TL;DR

The DOMContentLoaded event fires when the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading. The load event, on the other hand, fires when the entire page, including all dependent resources such as stylesheets and images, has finished loading.

document.addEventListener('DOMContentLoaded', function () {
console.log('DOM fully loaded and parsed');
});
window.addEventListener('load', function () {
console.log('Page fully loaded');
});

Difference between document load event and document DOMContentLoaded event

DOMContentLoaded event

The DOMContentLoaded event is fired when the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading. This event is useful when you want to execute JavaScript code as soon as the DOM is ready, without waiting for all resources to be fully loaded.

document.addEventListener('DOMContentLoaded', function () {
console.log('DOM fully loaded and parsed');
});

load event

The load event is fired when the entire page, including all dependent resources such as stylesheets, images, and subframes, has finished loading. This event is useful when you need to perform actions that require all resources to be fully loaded, such as initializing a slideshow or performing layout calculations that depend on image sizes.

window.addEventListener('load', function () {
console.log('Page fully loaded');
});

Key differences

  • Timing: DOMContentLoaded fires earlier than load. DOMContentLoaded occurs after the HTML is fully parsed, while load waits for all resources to be loaded.
  • Use cases: Use DOMContentLoaded for tasks that only require the DOM to be ready, such as attaching event listeners or manipulating the DOM. Use load for tasks that depend on all resources being fully loaded, such as image-dependent layout calculations.

Further reading

Why is it generally a good idea to position CSS `<link>`s between `<head></head>` and JS `<script>`s just before `</body>`?

Do you know any exceptions?
Topics
HTMLPerformance

In a nutshell, such a placement of CSS <link>s and JavaScript <script>s allows for faster rendering of the page and better overall performance.

Placing <link>s in <head>

Putting <link>s in <head> is part of the proper specification in building an optimized website. When a page first loads, HTML and CSS are being parsed simultaneously; HTML creates the DOM (Document Object Model) and CSS creates the CSSOM (CSS Object Model). Both are needed to create the visuals in a website, allowing for a quick "first meaningful paint" timing. Placing CSS <link>s in the <head> ensures that the stylesheets are loaded and ready for use when the browser starts rendering the page.

This progressive rendering is a metric that sites are measured on in their performance scores. Putting stylesheets near the bottom of the document is what prohibits progressive rendering in many browsers. Some browsers block rendering to avoid having to repaint elements of the page if their styles change. The user is then stuck viewing a blank white page. Other times there can be flashes of unstyled content (FOUC), which show a webpage with no styling applied.

Placing <script>s just before </body>

<script> tags block HTML parsing while they are being downloaded and executed, which can slow down the display of your page. Placing the <script>s at the bottom will allow the HTML to be parsed and displayed to the user first.

An exception to positioning <script>s at the bottom is when your script contains document.write(), but these days it's not good practice to use document.write(). Also, placing <script>s at the bottom means that the browser cannot start downloading the scripts until the entire document is parsed. This ensures that code which needs to manipulate DOM elements will not throw an error and halt the entire script.

Keep in mind that putting scripts just before the closing </body> tag creates the illusion that the page loads faster on an empty cache (since the scripts won't block downloading the rest of the document). However, if you have code you want to run during page load, it will only start executing after the entire page has loaded. If you put those scripts in the <head> tag, they would start executing earlier — so on a primed cache, the page would actually appear to load faster.

<head> and <body> tags are now optional

As per the HTML5 specification, certain HTML tags like <head> and <body> are optional. Google's style guide even recommends removing them to save bytes. However, this practice is still not widely adopted, and the performance gain is likely minimal — for most sites, it's not going to matter.

Consider HTML5 as an open web platform. What are the building blocks of HTML5?

Topics
BrowserHTML
  • Semantics: HTML tags describe the content.
  • Styling: Customizing the appearance of HTML tags.
  • Connectivity: Communicating with the server in new and innovative ways.
  • Offline and storage: Allows webpages to store data locally on the client and operate offline more efficiently.
  • Multimedia: Makes video and audio first-class citizens on the Open Web.
  • 2D/3D graphics and effects: Allows a much more diverse range of presentation options.
  • Performance and integration: Provides greater speed optimization and better usage of computer hardware.
  • Device access: Allows for the usage of various input and output devices.

What are `data-` attributes good for?

Topics
Web APIsHTMLTesting

Before JavaScript frameworks became popular, developers used data- attributes to store extra data within the DOM itself, without resorting to hacks like non-standard attributes or extra properties on the DOM. They are intended to store custom data private to the page or application, for when there are no more appropriate attributes or elements.

Another common use case for data- attributes is to store information used by third-party libraries or frameworks. For example, the Bootstrap library uses data attributes to cause <button>s to trigger actions on a modal elsewhere on the page (example).

<button type="button" data-bs-toggle="modal" data-bs-target="#myModal">
Launch modal
</button>
...
<div class="modal fade" id="myModal">Modal contents</div>

These days, using data- attributes is generally not encouraged. One reason is that users can easily modify the data attribute by using "inspect element" in the browser. The data model is better stored within the JavaScript environment and kept in sync with the DOM via virtual DOM reconciliation or two-way data binding, possibly through a library or a framework.

However, one perfectly valid use of data attributes is to add an identifier for end-to-end testing frameworks (e.g. Playwright, Puppeteer, Selenium), without adding classes or ID attributes just for tests when those attributes are primarily used for other purposes. The element needs a way to be selected, and something like data-test-id="my-element" is a valid way to do so without convoluting the semantic markup.

What is progressive rendering?

Topics
HTML

Progressive rendering is the name given to techniques used to improve the performance of a webpage (in particular, improve perceived load time) to render content for display as quickly as possible.

It used to be much more prevalent in the days before broadband internet, but it is still used in modern development as mobile data connections have become increasingly common (and remain unreliable).

Lazy loading of images

Images on the page are not loaded all at once. An image is only loaded when the user scrolls into or near the part of the page that displays it.

  • <img loading="lazy"> is a modern way to instruct the browser to defer loading of images that are outside the screen until the user scrolls near them.
  • Use JavaScript to watch the scroll position and load the image when it is about to come on screen (by comparing the coordinates of the image with the scroll position).

Prioritizing visible content (or above-the-fold rendering)

Include only the minimum CSS, content, and scripts necessary to display the portion of the page visible in the user's browser as quickly as possible. You can then use deferred scripts or listen for the DOMContentLoaded/load event to load in other resources and content.

Async HTML fragments

Flushing parts of the HTML to the browser as the page is constructed on the back end. More details on the technique can be found in this article.

Other modern techniques

Why you would use a `srcset` attribute in an image tag?

Explain the process the browser uses when evaluating the content of this attribute.
Topics
HTML

You would use the srcset attribute when you want to serve different images to users depending on their device display width. Serving higher-quality images to devices with retina displays enhances the user experience, while serving lower-resolution images to low-end devices improves performance and reduces data wastage (because serving a larger image will not have any visible difference). For example, <img srcset="small.jpg 500w, medium.jpg 1000w, large.jpg 2000w" src="..." alt=""> tells the browser to display the small, medium, or large .jpg graphic depending on the client's resolution. The first value is the image name and the second is the width of the image in pixels. For a device width of 320px, the following calculations are made:

  • 500 / 320 = 1.5625
  • 1000 / 320 = 3.125
  • 2000 / 320 = 6.25

If the client's resolution is 1x, 1.5625 is the closest, and 500w (corresponding to small.jpg) will be selected by the browser.

If the resolution is retina (2x), the browser will use the closest resolution above the minimum — meaning it will not choose 500w (1.5625) because it is greater than 1 and the image might look bad. The browser would then choose the image with a resulting ratio closer to 2, which is 1000w (3.125).

srcset solves the problem of serving smaller image files to narrow-screen devices, as they don't need huge images like desktop displays do — and optionally, of serving different-resolution images to high- and low-density screens.

Explain what a single page app is and how to make one SEO-friendly

Topics
JavaScriptHTML

TL;DR

A single page application (SPA) is a web application that loads a single HTML document and updates its content in the browser via JavaScript, rather than requesting a new page from the server on each navigation. This model provides application-like UX but presents challenges for search engine indexing because the initial HTML does not contain the rendered content.

In current practice, SPAs are made SEO-friendly by producing HTML on the server rather than relying solely on client-side rendering. The available strategies are server-side rendering (SSR), static site generation (SSG), incremental static regeneration (ISR), and streaming with React Server Components. Each strategy offers a different tradeoff between freshness, server cost, and time to first paint, and modern frameworks such as Next.js, Nuxt, Remix, SvelteKit, and Astro support selecting the strategy per route.


What is a single page app?

A SPA is a web application that loads a single HTML document and performs navigation and content updates on the client via JavaScript. The initial page load retrieves the HTML shell and application bundle; subsequent user actions update the DOM in place rather than triggering full-page navigations.

Key characteristics:

  • A single HTML document is served on initial load; subsequent views are rendered client-side.
  • The fetch API (or XMLHttpRequest) is used to communicate with the server without full-page reloads.
  • A client-side router (for example, react-router or vue-router) maps URL changes to view transitions.
  • Application state is typically held in memory rather than stored per request.

Benefits:

  • Smoother navigation after the initial load.
  • Reduced server load, because subsequent navigations do not require rendering a new page.
  • Application-like interaction patterns, such as preserved state across route transitions.

How search engines render JavaScript

The statement "Google cannot index JavaScript" is out of date. The practical situation is more nuanced:

  • Googlebot executes JavaScript using an evergreen Chromium-based renderer. The bot fetches HTML first, and pages requiring JavaScript rendering are added to a separate render queue. The render queue has improved substantially since 2019, but indexing of JS-rendered content is typically slower than indexing of server-rendered HTML.
  • Rendering is separate from crawling. This two-phase model means JS-rendered content can appear in the index later than its server-rendered counterpart, which is a disadvantage for time-sensitive content or highly competitive queries.
  • Other search engines render JavaScript less reliably. Bing's JavaScript rendering is less consistent than Google's, and other engines such as Yandex and Baidu have more limited support. SSR or prerendering is therefore still relevant when non-Google traffic is important.
  • Social and preview scrapers do not execute JavaScript. Clients that fetch OpenGraph and Twitter Card metadata — including Slack, LinkedIn, Discord, and Facebook — read the initial HTML only. Meta tags that are injected by client-side code are not visible to these clients.

For details, see Google's JavaScript SEO documentation and the Google Search Central documentation on rendering.

Rendering strategies

Modern frameworks allow different strategies to be used on different routes within the same application.

Server-side rendering (SSR)

The server renders the HTML for each request using the current application state. This produces indexable HTML on first response and reflects up-to-date data, at the cost of per-request server rendering.

Example with the Next.js App Router:

// app/products/[id]/page.tsx — a React Server Component, async by default
export default async function ProductPage({ params }) {
const res = await fetch(`https://api.example.com/products/${params.id}`, {
cache: 'no-store',
});
const product = await res.json();
return (
<div>
<h1>{product.title}</h1>
<p>{product.description}</p>
</div>
);
}

The App Router, introduced in Next.js 13, replaces the getServerSideProps data-fetching function of the Pages Router with async Server Components. cache: 'no-store' opts out of the framework's data cache to produce a fresh render on each request.

Static site generation (SSG)

HTML is produced at build time and served as static files. This is the cheapest option to host, produces the fastest first-byte response, and is suitable for content that does not change per request.

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then((r) =>
r.json(),
);
return posts.map((p) => ({ slug: p.slug }));
}
export default async function Post({ params }) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(
(r) => r.json(),
);
return <article>{post.body}</article>;
}

Incremental static regeneration (ISR)

Pages are generated statically and cached, with a configurable revalidation interval. The cached HTML is served immediately; when the revalidation interval expires, the framework regenerates the page in the background on the next request. This combines SSG's serving cost with tunable freshness.

// app/categories/[slug]/page.tsx
export default async function Category({ params }) {
const res = await fetch(`https://api.example.com/categories/${params.slug}`, {
next: { revalidate: 3600 },
});
const category = await res.json();
return <CategoryView data={category} />;
}

React Server Components with streaming

React Server Components execute on the server and ship as a serialized tree rather than HTML. Combined with Suspense boundaries, the framework can stream the HTML shell early and defer slower sections until their data resolves. This is useful for pages with a fast critical section and slower below-the-fold content.

import { Suspense } from 'react';
export default function Feed() {
return (
<>
<Header />
<Suspense fallback={<FeedSkeleton />}>
<SlowFeed />
</Suspense>
</>
);
}

Client-side rendering (CSR)

The HTML shell is served, and the full view is rendered by JavaScript in the browser. This remains appropriate for views that are not intended to be indexed, such as authenticated dashboards and internal tools, where SSR adds server cost without SEO benefit.

Choosing a strategy per route

The appropriate strategy depends on the route's data characteristics and indexing requirements. A typical allocation is:

Use caseRecommended strategyRationale
E-commerce product pageISR, short revalidation windowPrices and inventory change, but not per visit; cached HTML improves Largest Contentful Paint
Marketing site, documentation, blogSSGNo per-request variability; suitable for CDN distribution
Dashboard behind authenticationCSRNot indexed; SSR provides no SEO benefit and increases server cost
Personalized feed or homepageRSC with streamingFast shell response; personalized content is streamed as it resolves
Search results pageSSRQuery-dependent output that should be indexable for long-tail queries
Real-time dashboardCSRData changes more frequently than server HTML can be regenerated usefully
Breaking news articleSSR or short-window ISRFreshness is important; SSR under traffic spikes, ISR otherwise

Core Web Vitals comparison

The rendering strategy affects the metrics used by search ranking and perceived performance. The following table gives illustrative ranges for a content page with approximately 100 KB of data, measured on a slow mobile connection. Actual values depend on payload size, server region, and CDN configuration, and should be measured against the specific deployment.

StrategyTTFBLCPNotes
CSRLowHighFast first byte (shell only); LCP blocked by JS download and API round trip
SSRModerateLowSlower first byte due to server rendering; content visible sooner
SSGLowLowCDN-cached HTML; typically the fastest overall
ISRLowLowServed from CDN cache; regenerated in background on revalidation
RSC streamingLowLowShell streams first; Suspense boundaries hydrate as data resolves

Tools such as PageSpeed Insights and WebPageTest provide lab measurements against a specific URL.

Framework coverage

Several frameworks support the strategies above:

  • Next.js (React) — App Router is the current default. Supports SSR, SSG, ISR, and React Server Components with streaming.
  • Remix (React) — Emphasizes web standards and nested routing with loader and action functions. Merged with React Router in 2024.
  • Nuxt (Vue) — Supports SSR, SSG, ISR (via the Nitro server), and hybrid rendering.
  • SvelteKit (Svelte) — Adapter-based; deploys as SSR, SSG, or edge functions depending on configuration.
  • Astro — Island architecture. Ships zero JavaScript by default and hydrates only the components marked as interactive. Well suited to content-heavy sites with limited interactivity.
  • SolidStart (Solid) — Architecturally similar to SvelteKit with Solid's fine-grained reactivity.

General guidance for new projects:

  • Content-heavy sites with limited interactivity: Astro.
  • React applications with a mix of interactive and indexable routes: Next.js or Remix.
  • Vue applications: Nuxt.

A framework with SSR or SSG support is preferable to pure client-side rendering when SEO is a requirement, even for applications that would otherwise be implemented as a traditional SPA.

Common misconceptions

  1. "SSR means no JavaScript on the client." SSR produces server-rendered HTML, but the client still downloads and hydrates the JavaScript bundle to attach event handlers. The bundle size is generally comparable to the CSR equivalent.
  2. "CSR is not SEO-friendly." Googlebot indexes content rendered by JavaScript. The practical concerns are latency, indexing reliability, and compatibility with non-Google engines and social scrapers. For high-competition queries these concerns are significant; for long-tail content they may be acceptable.
  3. "SSR should be used for everything." SSR has a per-request CPU cost. For content that does not vary per request, SSG is substantially cheaper to serve. Defaulting to SSR when SSG would suffice increases hosting cost without benefit.
  4. "Hydration is free." Hydration re-executes the component tree on the client to attach event handlers. Large hydration trees can affect Interaction to Next Paint (INP) and other interactivity metrics. React Server Components reduce hydration cost by allowing portions of the tree to remain server-only.

Further reading

When would you use `document.write()`?

Topics
Web APIsJavaScriptHTML

TL;DR

document.write() is rarely used in modern web development because it can overwrite the entire document if called after the page has loaded. It is mainly used for simple tasks like writing content during the initial page load, such as for educational purposes or quick debugging. However, it is generally recommended to use other methods like innerHTML, appendChild(), or modern frameworks for manipulating the DOM.


When would you use document.write()?

Initial page load

document.write() can be used to write content directly to the document during the initial page load. This is one of the few scenarios where it might be appropriate, as it can be simpler and faster for very basic tasks.

<!doctype html>
<html>
<head>
<title>Document Write Example</title>
</head>
<body>
<script>
document.write('<h1>Hello, World!</h1>');
</script>
</body>
</html>

Educational purposes

document.write() is sometimes used in educational contexts to demonstrate basic JavaScript concepts. It provides a straightforward way to show how JavaScript can manipulate the DOM.

Quick debugging

For quick and dirty debugging, document.write() can be used to output variables or messages directly to the document. However, this is not recommended for production code.

var debugMessage = 'Debugging message';
document.write(debugMessage);

Legacy code

In some older codebases, you might encounter document.write(). While it's not recommended to use it in new projects, understanding it can be useful for maintaining or refactoring legacy code.

Why not use document.write()?

  • Overwrites the document: If called after the page has loaded, document.write() will overwrite the entire document, which can lead to loss of content and a poor user experience.
  • Better alternatives: Modern methods like innerHTML, appendChild(), and frameworks like React or Vue provide more control and are safer to use.
// Using innerHTML
document.getElementById('content').innerHTML = '<h1>Hello, World!</h1>';
// Using appendChild
var newElement = document.createElement('h1');
newElement.textContent = 'Hello, World!';
document.getElementById('content').appendChild(newElement);

Further reading

What kind of things must you be wary of when designing or developing for multilingual sites?

Topics
HTMLInternationalization

Designing and developing for multilingual sites is part of internationalization (i18n).

Search Engine Optimization

  • Use the lang attribute on the <html> tag.
  • Include the locale in the URL (e.g. en_US, zh_CN, etc.).
  • Webpages should use <link rel="alternate" hreflang="other_locale" href="url_for_other_locale"> to tell search engines that there is another page at the specified href with the same content but for another language/locale.
  • Use a fallback page for unmatched languages. Use the x-default value: <link rel="alternate" href="url_for_fallback" hreflang="x-default" />.

Understanding the difference between locale vs language

Locale settings control how numbers, dates, and times display for your region, which may be a country, a portion of a country, or may not even honor country boundaries.

Language can differ between countries

Certain languages, especially the widely-spoken languages have different "flavors" in different countries (grammar rules, spelling, characters). It's important to differentiate languages for the target country and not assume/force one country's version of a language for all countries which speak the language. Examples:

  • en: en-US (American English), en-GB (British English)
  • zh: zh-CN (Chinese (Simplified)), zh-TW (Chinese (Traditional))

Predict locale but don't restrict

Servers can determine the locale/language of visitors via a combination of HTTP Accept-Language headers and IPs. With these, servers can automatically select the best locale for the visitor. However, predictions are not foolproof (especially if visitors are using VPNs) and visitors should still be allowed to change their country/language easily without hassle.

Consider differences in the length of text in different languages

Some content can be longer when written in another language. Be wary of layout or overflow issues in the design. It's best to avoid designing where the amount of text would make or break a design. Character counts come into play with things like headlines, labels, and buttons. They are less of an issue with free-flowing text such as body text or comments. For example, some languages, such as German and French, tend to use longer words and sentences than English, which can cause layout issues if you do not take this into account.

Language reading direction

Languages like English and French are written from left to right, top to bottom. However, some languages, such as Hebrew and Arabic, are written from right to left. This can affect the layout of your site and the placement of elements on the page, so you must be careful to design your site in a way that accommodates different text directions.

Do not concatenate translated strings

Formatting dates and currencies

Calendar dates are sometimes presented in different ways. E.g. "May 31, 2012" in the U.S. vs. "31 May 2012" in parts of Europe.

Do not put text in images

Putting text in raster-based images (e.g. PNG, GIF, JPG) is not a scalable approach. Placing text in an image is still a popular way to get good-looking, non-system fonts to display on any computer. However, to support image text translation in other languages, a separate image must be created for each language, which is not a scalable workflow for designers.

Be mindful of how colors are perceived

Colors are perceived differently across languages and cultures. The design should use color appropriately.

References

How do you serve a page with content in multiple languages?

Topics
HTMLInternationalization

Serving a page in different languages is one of the aspects of internationalization (i18n).

When an HTTP request is made to a server, the requesting user agent usually sends information about language preferences, such as in the Accept-Language header. The server can then use this information to return a version of the document in the appropriate language if such an alternative is available. The returned HTML document should also declare the lang attribute in the <html> tag, such as <html lang="en">...</html>.

To let a search engine know that the same content is available in different languages, <link> tags with the rel="alternate" and hreflang="..." attributes should be used. E.g. <link rel="alternate" hreflang="de" href="http://de.example.com/page.html" />.

Rendering

  • Server-side rendering: The HTML markup will contain string placeholders, and content for the specific language will be fetched from configuration in code or from a translation service. The server then dynamically generates the HTML page with content in that particular language.
  • Client-side rendering: The appropriate locale strings will be fetched and combined with the JavaScript-based views.

What does a doctype do?

Topics
HTML

doctype is an abbreviation for Document Type. A doctype is always associated with a DTD — short for Document Type Definition.

A DTD defines how documents of a certain type should be structured (i.e. a button can contain a span but not a div), whereas a doctype declares what DTD a document supposedly follows (i.e. this document follows the HTML DTD).

For webpages, the doctype declaration is required. It is used to tell user agents what version of the HTML specification your document follows. Once a user agent has recognized a correct doctype, it will trigger the no-quirks mode matching that doctype for reading the document. If a user agent doesn't recognize a correct doctype, it will trigger quirks mode.

The doctype declaration for the HTML5 standard is <!doctype html>.

Why it matters

Without doctype:

  • Box model calculations differ
  • CSS rendering inconsistencies
  • JavaScript behavior variations
  • Layout problems across browsers

With proper doctype:

  • Predictable CSS box model
  • Consistent rendering
  • Modern HTML5 features work properly
  • Better cross-browser compatibility
Describe the difference between `<script>`, `<script async>` and `<script defer>`