Category: CSS & UI Engineering

Modern CSS, design systems, component styling, animations, layout techniques, and accessibility (a11y) implementation.

  • Optimize Next.js Images for Performance

    Optimize Next.js Images for Performance

    Short answer: To optimize Next.js images for performance, use the built-in next/image component, which automatically serves responsive images, lazy loads them, and converts to modern formats like WebP. Configure remote URLs in next.config.js, set appropriate sizes, and use priority for above-the-fold images.

    Key takeaways

    • Always use next/image for automatic optimization.
    • Enable lazy loading for all images by default.
    • Configure remote hostnames in next.config.js.
    • Set explicit width and height to prevent layout shifts.
    • Use priority for above-the-fold images.
    • Serve modern formats like WebP and AVIF.

    Images are often the heaviest assets on a web page. In Next.js, you can optimize images for performance using the built-in next/image component. This component automatically applies lazy loading, responsive sizing, and modern image formats. In this article, you will learn practical steps to optimize Next.js images and improve your site’s speed and user experience.

    Why Image Optimization Matters in Next.js

    Large images slow down page load times and hurt Core Web Vitals, especially Largest Contentful Paint (LCP). Next.js provides the next/image component to handle optimization automatically. It resizes images, serves them in next-gen formats like WebP and AVIF, and lazy loads them by default. This reduces bandwidth usage and improves performance.

    Without optimization, a 2 MB hero image can delay your LCP by several seconds. With next/image, the same image can be served as a 200 KB WebP file, dramatically improving load time.

    A digital illustration of image compression and optimization icons
    Understanding image optimization is key to performance. — Photo: Firmbee / Pixabay

    Getting Started with next/image

    To use the component, import Image from next/image. You can use it for local images stored in your project or remote images from external URLs.

    Using with Local Images

    For local images, just import the file and pass it to the src prop. Next.js automatically optimizes these images.

    import Image from 'next/image';
    import heroImage from '../public/hero.jpg';
    
    export default function Page() {
      return (
        <Image
          src={heroImage}
          alt="Hero image"
          width={1200}
          height={600}
        />
      );
    }
    

    Using with Remote Images

    For remote images, you need to configure the allowed hostnames in next.config.js. This is a security measure.

    // next.config.js
    module.exports = {
      images: {
        remotePatterns: [
          {
            protocol: 'https',
            hostname: 'example.com',
            port: '',
            pathname: '/images/**',
          },
        ],
      },
    };
    

    Then you can use the remote URL in the src prop. Note that you must provide width and height to prevent layout shifts.

    How Lazy Loading Improves Performance

    The next/image component lazy loads images by default. This means images are only loaded when they enter the viewport, saving bandwidth on initial page load. For above-the-fold images, you can disable lazy loading using the priority prop.

    Lazy loading is implemented using the native loading="lazy" attribute and Intersection Observer. It ensures that below-fold images do not block the initial render.

    Setting Responsive Sizes and Breakpoints

    To serve the right image size for each device, use the sizes and fill props. The sizes prop tells the browser how large the image will be at different breakpoints, helping select the best source.

    <Image
      src="/hero.jpg"
      alt="Hero"
      fill
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
    />
    

    The fill prop makes the image fill its parent container. You must set the parent to position: relative and define its size.

    A responsive image displayed on different device screens for optimization
    Use responsive sizes to serve the right image for each device. — Photo: gefrorene_wand / Pixabay

    Using Priority for Above-the-Fold Images

    The priority prop tells Next.js to preload the image and skip lazy loading. Use it for the largest above-the-fold image, usually the hero image. This improves LCP because the image starts loading immediately.

    <Image
      src="/hero.jpg"
      alt="Hero"
      width={1200}
      height={600}
      priority
    />
    

    Only use priority for one or two images to avoid overwhelming the network.

    Comparing Image Formats: JPEG vs WebP vs AVIF

    Modern formats like WebP and AVIF provide better compression than JPEG. Next.js automatically negotiates the format based on the browser’s Accept header. You can control this with the formats configuration.

    Format Compression Browser Support Recommended Use
    JPEG Good All Fallback
    WebP Better Widely supported Primary format
    AVIF Best Supported in many modern browsers For supported browsers

    By default, Next.js offers WebP and AVIF. You can customize the allowed formats in next.config.js.

    Additional Optimization Techniques

    Image Caching and CDN

    Next.js caches optimized images in the .next/cache/images directory. For production, consider using a CDN to serve images with cache headers. You can configure custom image loaders to use a CDN like Cloudinary or Imgix.

    Blur Up and Placeholder

    The placeholder prop allows you to show a blurred version of the image while it loads. This improves perceived performance. Use blur for local images or data URL for remote images.

    <Image
      src={heroImage}
      alt="Hero"
      placeholder="blur"
    />
    

    Common Mistakes to Avoid

    Many developers forget to configure remotePatterns for external images, causing errors. Others omit width and height, leading to layout shifts. Avoid using large images without setting sizes, as the browser may download oversized images. Also, do not apply priority to every image; it defeats lazy loading.

    Always test your images with Lighthouse to verify optimization. Check the “Properly size images” audit to see if you serve correctly sized images.

    How to Debug Image Optimization Issues

    When images don’t look right or performance isn’t improving, you can debug using Next.js’s built-in image optimization endpoint. Check the URL of the optimized image: it usually has /_next/image?url=.... If the image is not being optimized, ensure you are using the Image component and not a regular <img> tag. For remote images, verify that the hostname is in remotePatterns and that the image URL is accessible. Use the browser’s network tab to inspect the image request and response headers. Look for content-type to confirm it’s WebP or AVIF. Also check the response status — a 404 means the URL is wrong or the image doesn’t exist.

    Another common issue is that the image size doesn’t match the layout. Use the onLoadingComplete callback to get the actual image dimensions and compare them with what you expect. This helps identify if sizes or width and height are misconfigured.

    Optimizing for Different Screen Sizes and Devices

    To deliver the best experience across mobile, tablet, and desktop, combine the sizes prop with CSS media queries. The sizes attribute tells the browser what width the image will display at each breakpoint. For example, for a full-width hero on mobile and a half-width image on desktop, use sizes="(max-width: 768px) 100vw, 50vw". Always test on real devices to confirm the correct image is downloaded. You can check the image’s “current src” in the browser’s inspector to see which URL the browser selected. If it picks a too-large image, your sizes attribute might be too broad.

    Remember that the Image component generates multiple resized versions of each source image. The default device sizes are: 640, 750, 828, 1080, 1200, 1920, 2048, and 3840 pixels. You can customize these in next.config.js with the imageSizes and deviceSizes options. Adjust them based on your breakpoints and the maximum width of your layout.

    Frequently asked questions

    Does next/image automatically convert images to WebP?

    Yes, the next/image component automatically serves images in WebP and AVIF formats when the browser supports them. It negotiates the format based on the Accept header, so you don’t need to manually convert images.

    Do I need to set width and height for all next/images?

    Yes, you should always set width and height to prevent layout shifts and improve Cumulative Layout Shift (CLS). The only exception is when using the fill prop, where the parent container defines the dimensions.

    How do I use next/image with images from a CMS?

    You can use the remote image approach: configure the CMS domain in remotePatterns in next.config.js, then pass the image URL to the src prop. Ensure you provide width, height, and alt text.

    Can I use next/image with SVG files?

    Yes, but the next/image component does not optimize SVGs because they are vector-based. It will pass the SVG through without resizing or converting formats. You can use an tag or inline instead.

    What is the priority prop and when should I use it?

    The priority prop tells Next.js to preload the image and skip lazy loading. Use it for the largest above-the-fold image (usually the hero) to improve LCP. Avoid using it on multiple images to prevent unnecessary bandwidth usage.

  • Accessibility Checklist for React Applications

    Accessibility Checklist for React Applications

    Short answer: An accessibility checklist for React applications covers semantic HTML, proper ARIA usage, keyboard navigation, focus management, color contrast, form labeling, and automated testing. It helps developers systematically build inclusive web apps that meet WCAG standards.

    Key takeaways

    • Use semantic HTML elements instead of divs.
    • Manage focus order and visible focus indicators.
    • Add ARIA roles only when native semantics are insufficient.
    • Ensure keyboard navigation works for all interactive elements.
    • Test with screen readers and automated tools.
    • Maintain sufficient color contrast ratios.

    Building accessible React applications isn’t just about compliance. It’s about creating experiences that work for everyone, including people who rely on screen readers, keyboard navigation, or magnified displays. Accessibility, often shortened to a11y, should be part of your development workflow from the start. This checklist covers the essential areas you need to address.

    Why Accessibility Matters in React

    React renders dynamic interfaces that can easily break accessibility if you’re not careful. SPA routing, conditional rendering, and complex state changes can confuse assistive technologies. A structured checklist helps you catch issues early and maintain a high standard of usability.

    Accessible apps also benefit search engine SEO and provide a better experience for all users. Many accessibility practices align with good UX principles. By following this checklist, you’ll build more robust and maintainable components.

    Semantic HTML in React Components

    Start by using the correct HTML elements for their intended purpose. Use <nav> for navigation, <main> for primary content, <button> for actions, and <a> for links. Avoid unnecessary <div> or <span> when native elements exist.

    When creating custom components, ensure they render semantic markup. For example, a custom Button component should render a <button> element, not a <div> with an onClick handler. This preserves built-in keyboard and screen reader behavior.

    Use headings (<h1> through <h6>) in a logical order to structure content. Don’t skip levels, and avoid using headings for styling alone. Screen reader users rely on heading structure to navigate pages efficiently.

    One common mistake is using <div> for clickable cards or panels. Instead, consider using a <button> if it triggers an action, or wrap the entire card in an <a> if it navigates. If the card contains multiple actions, structure it with focusable elements inside.

    Developer testing accessibility on a laptop with keyboard and screen reader
    Testing keyboard navigation and screen reader compatibility — Photo: analogicus / Pixabay

    Keyboard Navigation and Focus Management

    All interactive elements must be reachable and operable via keyboard. Use the tab order consistently. Ensure that focus moves in a logical sequence as users tab through the page. Avoid tabindex values greater than 0.

    Manage focus when content changes dynamically. For instance, after a modal opens, move focus to the modal’s first focusable element. When the modal closes, return focus to the triggering button. React’s useRef and lifecycle methods make this manageable.

    Programmatic focus management: use element.focus() after state updates that add new navigation landmarks. For single-page app route changes, announce new content with aria-live regions or move focus to the top of the new view.

    For dynamic lists, such as search results that update as you type, ensure the first result receives focus or is announced. Consider using a useEffect that runs when results change to focus the new list item.

    Visible Focus Indicators

    Never remove the default focus outline without providing a custom visible indicator. Use CSS like :focus-visible to show focus only when navigating by keyboard. Ensure the focus style has a contrast ratio of at least 3:1 against its background.

    Design a focus ring that is noticeable but not overwhelming. A 2px solid outline with a 2px offset works well. Avoid using only color changes, as they may not be visible to users with low vision.

    ARIA Roles, States, and Properties

    ARIA (Accessible Rich Internet Applications) supplements native HTML semantics. Use it sparingly and correctly. The first rule of ARIA is not to use it if you can use a native HTML element that already has the needed semantics.

    When you need ARIA, add roles like role="button" only if the element isn’t naturally a button. For dynamic content, use aria-live regions to announce updates. For example, a live region on a shopping cart icon can announce “Your cart has 3 items” when the count changes.

    Manage ARIA attributes dynamically in React. Use state to toggle aria-expanded on accordion headers, aria-selected on tabs, and aria-hidden on off-screen panels. Ensure that ARIA states match the component’s visual state.

    Be careful with aria-live regions. Use polite for non-critical updates and assertive only for urgent warnings. Overusing assertive can be disruptive. Also, set aria-atomic="true" if the entire region should be announced as a whole.

    Color contrast checker tool on a monitor showing WCAG ratios
    Verifying color contrast ratios for accessible design — Photo: shogun / Pixabay

    Color Contrast and Visual Design

    Text and interactive elements must have sufficient color contrast. The WCAG 2.1 AA standard requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text (18px bold or 24px regular). Use tools like the WebAIM Contrast Checker to verify.

    Don’t convey information through color alone. For example, form errors should include an icon or text label, not just a red border. Charts and graphs should use patterns or labels in addition to color.

    Test your design under different color vision deficiencies. Simulators can show how your app looks to users with deuteranopia (red-green color blindness) or other conditions. Aim for clear differentiation without relying on hue.

    For interactive elements like links, use both underline and color to distinguish them from surrounding text. Hover and focus states should also be distinguishable. A good practice is to ensure that the same information is available through text or icons.

    Forms and Input Labels

    Every form input must have an associated label. Use the <label> element with a htmlFor attribute matching the input’s id. Alternatively, wrap the input inside the label. For custom-styled inputs, use aria-label or aria-labelledby.

    Provide clear error messages that are programmatically associated with the input using aria-describedby. For example, a required email field might have an error message like “Please enter a valid email address” linked via aria-describedby="email-error".

    Group related inputs with <fieldset> and <legend>. This is especially useful for radio buttons, checkboxes, and address fields. The legend provides a label for the group, helping screen reader users understand the context.

    For validation, use live regions to announce errors after submission. Avoid relying solely on visual cues. Also, ensure that error messages are concise and actionable, telling the user what to fix.

    Testing Accessibility in React

    Automated testing catches many common issues. Integrate tools like eslint-plugin-jsx-a11y into your IDE and CI pipeline. It flags missing alt text, incorrect ARIA, and invalid heading order. For unit tests, use jest-axe to run accessibility assertions on rendered components.

    Manual testing is equally important. Use your keyboard to navigate the entire app. Can you reach all links, buttons, and form fields? Does the tab order make sense? Also test with a screen reader like NVDA (Windows) or VoiceOver (macOS) to hear what users experience.

    Test on different devices and zoom levels. Ensure the app is usable at high zoom levels and on small screens. Accessibility overlaps significantly with responsive design and mobile usability.

    Create a testing checklist for each new feature. Include items like: “Does this component work with keyboard only?”, “Are focus states visible?”, “Is content announced correctly?” Run manual tests after every major update.

    Images and Alternative Text

    Every image must have appropriate alternative text. Use the alt attribute on <img> elements. For decorative images, set alt="" (empty alt) so screen readers ignore them. For informative images, describe the content concisely.

    In React, always pass an alt prop to your Image component. If the image is a link, the alt text should describe the link destination. For complex images like charts, provide a text alternative nearby or in a longdesc attribute.

    Use ARIA roles like role="img" on SVG icons if they convey meaning. Otherwise, add aria-hidden="true" to ignore them. For icon buttons, combine with aria-label to provide an accessible name.

    Putting It All Together

    Start integrating these checks into your development workflow. Choose a few items from this checklist and focus on them in your next sprint. Over time, accessibility becomes a natural part of your coding habits.

    Remember that accessibility is a continuous process. User needs change, and new tools emerge. Regularly revisit your checklist, run automated scans, and listen to feedback from users with disabilities. Your React app will be better for everyone.

    Frequently asked questions

    What is the most common accessibility mistake in React apps?

    The most common mistake is using unnecessary or incorrect ARIA attributes. Many developers add ARIA roles and properties when a native HTML element already provides the needed semantics. For example, using role=”button” on a <div> instead of using a <button> element. This breaks keyboard navigation and screen reader support.

    How do I manage focus in a single-page React app?

    When navigating between routes, focus should move to the new page’s heading or main content. Use a useEffect hook to call focus() on a ref after the route change. For modals or dialogs, trap focus inside the modal and return it to the trigger element on close. Libraries like react-focus-lock can help.

    Do I always need to use ARIA in React?

    No. The first rule of ARIA is to use native HTML elements when possible. For example, use <button> instead of <div role=”button”>. Use ARIA only when the native semantics are insufficient, such as for live regions, custom widgets, or when you need to overwrite an element’s implicit role.

    What tools can I use to test accessibility in React?

    Use eslint-plugin-jsx-a11y for static analysis in your editor. For unit tests, jest-axe runs Axe accessibility checks on rendered components. For end-to-end testing, tools like Cypress with cypress-axe or Playwright with Axe integration can catch issues during integration tests.

    How often should I run accessibility audits?

    Run automated accessibility checks continuously in your CI pipeline on every pull request. Perform manual keyboard and screen reader tests during feature development and before major releases. Schedule full audits (using tools like Lighthouse or axe DevTools) at least once per sprint or after significant UI changes.

  • Why Your React App Re-renders and How to Fix It

    Why Your React App Re-renders and How to Fix It

    Short answer: React re-renders when state, props, or context change. To fix unnecessary re-renders, use React.memo to memoize components, useMemo and useCallback to stabilize values and functions, and restructure your component tree to avoid broad context updates.

    Key takeaways

    • React re-renders when state, props, or context change.
    • Unnecessary re-renders happen when parent components update child components that don’t need to change.
    • React.memo prevents re-renders if props haven’t changed.
    • useMemo and useCallback stabilize values and functions across renders.
    • Avoiding inline functions and objects in JSX reduces re-renders.
    • Context updates cause all consumers to re-render; split contexts to minimize impact.

    You’ve built a React app that works. But as it grows, you notice it slowing down. Every keystroke lags, and the UI feels sluggish. The culprit is often unnecessary re-renders. In this article, you’ll learn exactly why React re-renders components and how to fix the most common causes. By the end, you’ll have a clear set of tools to make your React app fast and responsive.

    How React Decides to Re-render

    React re-renders a component when its state changes, when its parent re-renders, or when the context it consumes updates. When state changes inside a component, that component re-renders. Then, all its children re-render as well. This cascading behavior is by design: React ensures the UI is always in sync with the data.

    However, this default behavior can cause performance issues. If a parent re-renders, every child re-renders, even if their props haven’t changed. That’s where optimization comes in.

    Common Causes of Unnecessary Re-renders

    A developer looking at a laptop with a frustrated expression, symbolizing performance issues from re-renders
    Unnecessary re-renders can slow down your app. — Photo: Pexels / Pixabay

    Let’s look at the most frequent reasons for too many re-renders in production React apps.

    Inline Functions and Objects in JSX

    When you define a function or create an object directly inside JSX, you create a new reference on every render. For example:

    <Button onClick={() => handleClick(id)} />

    This inline arrow function is a new function every time. If Button is wrapped in React.memo, it will still re-render because the onClick prop reference changes. The same problem applies to inline objects like <Component style={{color: 'red'}} />. A common mistake is to think that React.memo alone solves everything — it doesn’t if you pass new references each time.

    To fix this, extract the inline callback or object outside the JSX, or use useCallback and useMemo to stabilize references. For example, instead of onClick={() => handleClick(id)}, you can define const handleClickCallback = useCallback(() => handleClick(id), [id]); and pass handleClickCallback to the button.

    State Updates That Don’t Change State

    Calling setState with the same value doesn’t cause a re-render in class components, but in functional components using hooks, it does. Every setState call triggers a re-render, even if the value is identical. This is a common source of extra renders when you accidentally update state too frequently, like in an event handler that runs multiple times. A real-world example: a dropdown that opens and closes — if you call setIsOpen(!isOpen) when it’s already open, the state changes and triggers an extra render. Check that you only call state setters when the value actually needs to change.

    Context Re-renders Everything

    When a context provider value changes, every consumer of that context re-renders. If you put too many values in one context, a change to one field triggers re-renders for components that only read the other fields. This is a subtle but major performance drain. To mitigate this, split your context into smaller contexts or use useMemo for the value object so that consumers only re-render when the parts they depend on change.

    For example, if you have a UserContext that holds both userName and theme, a change to theme will re-render components that only read userName. Instead, create separate UserNameContext and ThemeContext providers.

    Tools to Prevent Unnecessary Re-renders

    Diagram showing a tree of components with highlighted nodes indicating re-renders
    Understanding the component tree helps identify re-render patterns. — Photo: 777546 / Pixabay

    React gives you several tools to prevent unnecessary re-renders. Use them wisely — over-optimizing can make your code harder to read.

    Tool Use Case Important Note
    React.memo Prevent re-render of a component when its props haven’t changed. Only shallow-compares props. Use React.memo for pure presentational components that receive simple props. If props contain functions or objects, they must be stable references.
    useMemo Cache a computed value until its dependencies change. Use for expensive calculations, not trivial operations like useMemo(() => a + b, [a, b]) — that adds unnecessary overhead. Reserve it for array transformations, filtering, or complex data manipulations.
    useCallback Return a memoized function reference that only changes when dependencies change. Use when passing callbacks to child components wrapped in React.memo. Always list all dependencies used inside the callback to avoid stale closures.
    useRef Keep a mutable value across renders without causing re-renders. Excellent for timers, DOM references, or any value that changes but shouldn’t trigger a UI update. Unlike state, changing a ref does not trigger a re-render.

    Another important strategy is to lift state down. Instead of putting all state in a high-level component, keep state as close as possible to where it’s used. This reduces the number of components that need to re-render when that state changes. For example, if only one child needs a piece of state, store it in that child instead of the parent. If multiple children need it but not all, consider using a shared context with a granular provider.

    Step-by-Step: Fixing a Common Re-render Problem

    Let’s walk through a typical scenario. You have a parent component that holds a search query and a list of items. The list is filtered based on the query. Without optimization, every keystroke re-renders the entire list.

    1. Identify the problem: Profile your app using React DevTools to see which components re-render. You’ll likely see the list re-rendering on every input change. Open the Profiler tab, start recording, type in the search box, and stop. Look at the flamegraph to spot the list component highlighted as re-rendered.
    2. Memoize the list component: Wrap the list component in React.memo. Now it will only re-render if its props change.
    3. Stabilize the filter function: Use useMemo to compute the filtered list only when the search query or the original list changes. The filtered list reference stays stable if the query didn’t change. Example: const filteredList = useMemo(() => list.filter(item => item.name.includes(query)), [list, query]);
    4. Memoize the filter callback: If you pass a callback to a child, use useCallback to keep the reference stable. This is especially important if the child is wrapped in React.memo. For instance, a delete button inside each list item should receive a stable onDelete function.
    5. Test and profile again: Verify that the list no longer re-renders on unrelated state changes. Use the React DevTools Profiler again to confirm the list component is not re-rendering when only the query changes.

    Common pitfalls: forgetting to include all dependencies in useMemo or useCallback arrays, or wrapping everything in memo without measuring. Always check if the optimization actually reduces re-renders by using the profiler.

    When Not to Optimize

    Optimization has a cost. React.memo adds a prop comparison on every render. useMemo and useCallback add overhead for dependency tracking. Only optimize when you measure a performance problem. Premature optimization can make your code harder to maintain without real benefit.

    A good rule of thumb: focus on components that render often (like list items) or that have expensive render logic (like complex charts). For components that render infrequently or are cheap, let React do its job. Also, avoid wrapping a component in React.memo if it receives children or other JSX — those are objects and always change, defeating the purpose.

    How to Profile Re-renders Effectively

    Before you optimize, you need to know what to fix. Open React DevTools and go to the Profiler tab. Click the record button, interact with your app (like typing in an input), then stop. The profiler shows you all components that re-rendered during that interaction, with timing bars. Look for components that re-render frequently and take a long time. Also, enable the “Record why each component rendered” option to see the reason (state change, parent re-render, context change). This tells you exactly which hook call or prop change caused the re-render.

    Another technique: add console.log statements inside your component to track renders. For example, console.log('MyComponent rendered'). Just remember to remove them in production. This is a low-tech but effective way to spot unnecessary renders.

    For deeper analysis, you can use the why-did-you-render package, but it can be noisy. Only use it on specific components when you need to see why they re-render despite seemingly unchanged props.

    If you’re tackling performance, you might also want to explore lazy loading to reduce initial bundle size. Check out our guide on How to Improve Web Performance with Lazy Loading for techniques that complement re-render optimization.

    Another common issue is accessibility mistakes that accidentally cause re-renders, like using improper ARIA attributes. Read 10 Common Accessibility Mistakes Frontend Developers Make to avoid those pitfalls.

    And if you’re wondering whether React or a framework like Next.js is right for your next project, we compare them in React vs Next.js: Choosing the Right Framework for Your Project.

    Wrapping Up

    Understanding why React re-renders and how to prevent unnecessary ones is key to building performant apps. Start by identifying the worst offenders with React DevTools, then apply React.memo, useMemo, and useCallback where they provide measurable improvement. Always measure before and after. That way, you keep your code clean and your app fast.

    Frequently asked questions

    Does React re-render when props change?

    Yes, when a parent component re-renders, all its children re-render by default, regardless of whether their props have changed. React’s reconciliation algorithm then diffs the virtual DOM, but the re-render function still runs. To prevent this, use React.memo on child components so they only re-render when their props actually change.

    What is the difference between useMemo and useCallback?

    useMemo returns a memoized value from a function, while useCallback returns a memoized function itself. Both accept a dependencies array and only recompute when dependencies change. Use useMemo for expensive computed values, and useCallback when you want to pass a stable function reference to a child component that is wrapped in React.memo.

    Why does my React app re-render on every keystroke?

    This usually happens because your input’s onChange handler updates state in a parent component, causing that parent and all its children to re-render. To fix it, move the state and input logic into a separate component that handles only the input, or use React.memo on the list component to prevent it from re-rendering when the input changes.

    Can context cause unnecessary re-renders?

    Yes. When the value of a context provider changes, every component that consumes that context re-renders, even if they only use a part of the context that didn’t change. To mitigate this, split large contexts into smaller, focused ones, or use useMemo to create the context value so it only changes when necessary.

    Is it always good to use React.memo on every component?

    No. React.memo adds a shallow prop comparison on every render, which has its own cost. It’s best used on components that render often and receive stable props, or on components that have expensive render logic. For lightweight components or those that rarely re-render, the comparison overhead may outweigh the benefit.

  • Beginner’s Guide to CSS Custom Properties Variables

    Beginner’s Guide to CSS Custom Properties Variables

    Short answer: CSS custom properties, often called CSS variables, are entities defined by authors that contain specific values to be reused throughout a document. You declare them with a double hyphen prefix like –main-color: #333; and access them using the var() function, e.g., color: var(–main-color);. They support inheritance, scoping, and dynamic updates via JavaScript, making them more powerful than preprocessor variables.

    Key takeaways

    • CSS custom properties are natively supported in all modern browsers.
    • Use the — prefix to declare, var() to consume.
    • Scope is determined by the element cascade and inheritance.
    • They can be changed dynamically with JavaScript.
    • Great for theming without multiple stylesheet overrides.
    • Unlike preprocessor variables, they cascade and inherit.

    Have you ever found yourself repeating the same color, font size, or spacing value across dozens of CSS rules? It gets tedious fast. One small design change can force you to search and replace across multiple files. CSS custom properties, commonly called CSS variables, solve this problem elegantly. They let you store values in one place and reference them anywhere in your stylesheet. With native browser support now universal, there’s no reason not to use them.

    Designer adjusting colors on a website mockup with custom properties
    Using custom properties for theming — Photo: jamesmarkosborne / Pixabay

    What Are CSS Custom Properties?

    CSS custom properties are entities defined by CSS authors that hold specific values which can be reused throughout a document. They follow the same cascade and inheritance rules as any other CSS property. You define them using a double hyphen prefix like --primary-color, and you retrieve their values using the var() function.

    For example, you might set a brand color in a :root rule and then apply it wherever needed:

    :root {
      --primary-color: #3498db;
    }
    
    .button {
      background-color: var(--primary-color);
    }
    

    Now, if you ever want to change your primary color, you update it in one place. All references update automatically. No find-and-replace. No missed instances.

    Declaring and Using CSS Custom Properties

    Declaring a custom property is straightforward. You write a property name starting with two hyphens, then a colon, then any value that would be valid for a CSS property. The value can be a color, length, number, string, or even a complex expression.

    :root {
      --main-bg: #f5f5f5;
      --gutter: 16px;
      --font-stack: 'Helvetica', Arial, sans-serif;
    }
    

    To use these variables, you wrap the property name inside var():

    body {
      background-color: var(--main-bg);
      padding: var(--gutter);
      font-family: var(--font-stack);
    }
    

    The var() function also accepts a fallback value as a second argument. If the custom property isn’t defined, the fallback gets used:

    .card {
      border: 1px solid var(--border-color, #ddd);
    }
    

    This is especially useful when working with components from third-party libraries or when you want to provide sensible defaults.

    Scope and Inheritance

    One powerful feature of CSS custom properties is that they follow the cascade. Where you declare a custom property determines its scope. A property declared on :root is available globally. If you declare it on a specific element, it only applies to that element and its descendants.

    .alert {
      --alert-bg: #ffeeba;
      background-color: var(--alert-bg);
    }
    
    .alert .close-btn {
      color: var(--alert-bg); /* inherits from .alert */
    }
    

    This scoping behavior lets you create component-specific overrides without affecting other parts of the page. For example, you might define a default button style with global variables, then override those variables inside a special section to create a variant.

    :root {
      --btn-bg: #007bff;
      --btn-color: #fff;
    }
    
    .danger-zone {
      --btn-bg: #dc3545;
      --btn-color: #fff;
    }
    

    Now, any button inside .danger-zone will automatically use the red background, while buttons elsewhere keep the blue. No extra classes, no specificity battles.

    Dynamic Updates with JavaScript

    Unlike preprocessor variables (like those in Sass or Less), CSS custom properties can be changed at runtime via JavaScript. This opens up many possibilities for interactive themes, user preferences, and animations.

    To read a custom property value from an element:

    const root = document.documentElement;
    const primaryColor = getComputedStyle(root).getPropertyValue('--primary-color').trim();
    

    To set a custom property value:

    root.style.setProperty('--primary-color', '#e74c3c');
    

    With this technique, you can build a dark mode toggle that flips a set of variables:

    1. Define your light theme colors on :root.
    2. Define alternative dark theme values under a .dark-mode class.
    3. When the user clicks a toggle, add or remove the class on the body.
    4. The cascade updates all colors automatically.

    No need to write separate stylesheets or reload the page. The same principle applies to creating interactive dashboards, live previews, or adjusting spacing based on viewport size using JavaScript.

    JavaScript code changing a CSS custom property value
    Dynamically updating CSS variables with JavaScript — Photo: Pexels / Pixabay

    CSS Variables vs. Preprocessor Variables

    If you’ve used Sass or Less, you might wonder how CSS custom properties compare. Both help you avoid repetition, but they work differently. Let’s break it down.

    Feature CSS Custom Properties Preprocessor Variables (Sass)
    Runtime access Yes, with JavaScript No, compiled away
    Inheritance Follows CSS cascade Not inherited; lexical scope
    Dynamic theming Easy via class or JS Requires recompilation
    Media queries Can be used inside Can’t be used inside without workarounds
    Browser support Modern browsers Requires a preprocessor
    Computation No built-in math Can perform arithmetic

    The key takeaway is that preprocessor variables are great for build-time consistency, while CSS custom properties shine for runtime flexibility. For most projects, using both together makes sense: preprocessor variables for static values (like breakpoints or math calculations) and custom properties for dynamic theming.

    Practical Use Cases

    Consistent Spacing and Sizing

    Define a spacing scale as custom properties:

    :root {
      --space-xs: 4px;
      --space-sm: 8px;
      --space-md: 16px;
      --space-lg: 24px;
      --space-xl: 32px;
    }
    

    Then use them throughout:

    .card {
      padding: var(--space-md);
      margin-bottom: var(--space-lg);
    }
    .card-title {
      margin-bottom: var(--space-sm);
    }
    

    This ensures every component adheres to your spacing system. Adjusting the whole design’s rhythm becomes a one-line change.

    Theming

    As mentioned earlier, custom properties are perfect for theming. You can define a set of color variables and swap them by toggling a class on a parent element:

    .dark-theme {
      --bg: #222;
      --text: #eee;
      --link: #80bfff;
    }
    

    Apply the class to the body, and all child elements that use these variables update instantly.

    Component Variants

    Instead of using modifier classes that override specific properties, you can define variant variables:

    .btn--primary {
      --btn-bg: #007bff;
      --btn-color: #fff;
    }
    .btn--secondary {
      --btn-bg: #6c757d;
      --btn-color: #fff;
    }
    .btn {
      background-color: var(--btn-bg);
      color: var(--btn-color);
      padding: 0.5rem 1rem;
      border: none;
    }
    

    This reduces specificity issues because you’re only varying the variables, not overriding multiple rules.

    Responsive Design

    You can change custom property values inside media queries:

    :root {
      --gutter: 8px;
    }
    
    @media (min-width: 768px) {
      :root {
        --gutter: 16px;
      }
    }
    

    Now any element using --gutter will have more spacing on larger screens. This is cleaner than rewriting margins and paddings for each breakpoint.

    Working with calc() and Custom Properties

    Custom properties work well with the calc() function, enabling dynamic calculations. For example, you can combine a variable with a fixed unit:

    :root {
      --base-size: 1rem;
    }
    
    .title {
      font-size: calc(var(--base-size) * 2);
    }
    

    You can also use multiple variables together:

    :root {
      --column-count: 3;
      --gap: 1rem;
    }
    
    .grid {
      display: grid;
      grid-template-columns: repeat(var(--column-count), 1fr);
      gap: var(--gap);
    }
    

    This pattern makes it easy to adjust layouts by changing just the variable values. Note that you cannot use custom properties inside calc() without the var() wrapper, and the expression must be valid at runtime.

    Debugging Custom Properties

    When something goes wrong with custom properties, the browser’s developer tools are your best friend. In the Elements panel, you can inspect any element and see the computed value of --custom-property. If the value is blank or shows an error, you might have a typo or a scope issue. Another trick is to temporarily add a fallback to see if the property resolves correctly. For instance, changing var(--my-var) to var(--my-var, red) will paint the element red if the variable is missing, confirming the issue. Common problems include using the wrong case or referencing a variable that was never declared. Always double-check that you are using the exact same name, including hyphens and case.

    Common Pitfalls and Tips

    • Case sensitivity: --primary and --Primary are different. Stick to a consistent naming convention, like kebab-case.
    • Missing fallback leads to invalid values: If you use a custom property that isn’t defined and provide no fallback, the property becomes invalid and gets ignored. Always provide a fallback when there’s a chance the variable might not be set.
    • Cannot be used in media queries directly: Custom properties don’t work inside @media query conditions because they are resolved at runtime, not parse time. Use media query ranges or preprocessor variables for breakpoint values.
    • Inheritance surprises: Because custom properties inherit, a child element might pick up a parent’s variable unintentionally. Override it explicitly if needed.
    • Performance considerations: Browser engines handle custom properties efficiently. However, excessive use of var() inside animations or on many elements can cause slight layout recalculations. Measure if you notice jank.

    CSS custom properties are a mature, practical feature that can simplify your stylesheets and make them far more maintainable. Start small: extract your colors, fonts, and spacing into variables. Once you get the hang of it, explore dynamic theming and component variants. Your future self will thank you.

    Frequently asked questions

    What is the difference between CSS custom properties and variables in Sass?

    CSS custom properties are native to the browser and can be changed at runtime using JavaScript. They follow the cascade and inheritance. Sass variables are compiled away and cannot be accessed or changed after the stylesheet is built. Use CSS custom properties for dynamic values and Sass variables for static build-time values.

    How do I set a fallback value for a CSS custom property?

    Pass a second argument to the var() function: var(–custom-prop, fallback-value). If –custom-prop is not defined, the fallback-value is used. Ensure the fallback is syntactically valid for the property, or the entire declaration becomes invalid.

    Can CSS custom properties be used inside media queries?

    You cannot use a custom property as the condition in a @media query, because custom properties are resolved dynamically. However, you can change the value of a custom property inside a media query and then use that variable elsewhere in your styles.

    Do CSS custom properties have performance issues?

    Modern browsers handle custom properties very efficiently. In most cases, the performance impact is negligible. However, if you use var() in many properties that trigger layout or painting, and change those variables frequently (e.g., via JavaScript animations), it may cause extra work. Usually, the benefits outweigh the costs.

    How do I inspect CSS custom properties in browser DevTools?

    In Chrome DevTools, open the Styles panel and scroll to the bottom where custom properties are listed. You can also see them in the Computed panel. Firefox has a dedicated Variables view in the Inspector. Click on the var() function to see its definition or set new values.

  • How to Set Up a TypeScript Project from Scratch

    How to Set Up a TypeScript Project from Scratch

    Short answer: To set up a TypeScript project from scratch, install Node.js, create a project folder, run npm init, install TypeScript as a dev dependency, create a tsconfig.json file, write your first .ts file, and compile it using npx tsc. For Node.js projects, also install @types/node.

    Key takeaways

    • Install TypeScript via npm as a dev dependency
    • Configure tsconfig.json for strict mode
    • Use separate source and dist directories
    • For Node.js, install @types/node
    • Set up a build script in package.json
    • Enable source maps for easier debugging

    You want to add static typing to your JavaScript project. TypeScript gives you that safety net without sacrificing the flexibility of JS. Setting up a TypeScript project from scratch is straightforward once you understand the moving parts. This guide takes you through a minimal yet production-ready setup using Node.js and npm.

    Terminal window showing npm install typescript command
    Run npm install –save-dev typescript to add TypeScript to your project. — Photo: 547877 / Pixabay

    Why Set Up TypeScript from Scratch?

    Using a starter template or a framework like Next.js with built-in TypeScript support is convenient, but it hides the configuration details. When you set up TypeScript from scratch, you control every option. You understand the compiler, the module system, and how types integrate with your runtime. This knowledge helps you debug configuration issues later and tailor TypeScript to your exact needs.

    Starting from scratch also teaches you the minimal requirements. You can then scale up when your project demands stricter checks or more complex builds.

    Prerequisites

    You need Node.js installed on your machine. TypeScript runs on Node, and you’ll use npm to install it. If you haven’t already, download the latest LTS version from the official Node.js website. Verify your installation by running node --version and npm --version in your terminal.

    You should also be comfortable with basic terminal commands and have a code editor ready. VS Code is a popular choice because of its first-class TypeScript support.

    Step-by-Step: Initialize the Project

    Follow these steps to create a new TypeScript project from an empty folder.

    1. Create a project folder: mkdir my-typescript-project and cd my-typescript-project.
    2. Initialize npm: Run npm init -y to create a package.json file with default values. You can edit the fields later.
    3. Install TypeScript: npm install --save-dev typescript. Installing it as a dev dependency keeps it out of production builds.
    4. Create tsconfig.json: Run npx tsc --init to generate a boilerplate configuration file.
    5. Create your first TypeScript file: Make a src directory and add index.ts inside it with a simple function.
    6. Compile: Run npx tsc to compile the TypeScript files. By default, output goes to the same directory. We’ll change that soon.

    That’s the basic setup. But a real project needs a better structure. Let’s refine it.

    Code editor showing tsconfig.json file configuration
    Configure your tsconfig.json with strict mode and output directories. — Photo: Boskampi / Pixabay

    Configuring tsconfig.json for a Real Project

    The tsconfig.json file is the heart of your TypeScript configuration. Here are the key options you’ll want to set up for a typical Node.js project.

    Strict Mode

    Enable strict: true to catch more errors at compile time. This turns on all strict type-checking options, including noImplicitAny and strictNullChecks. The earlier you catch type issues, the fewer runtime surprises you’ll have.

    Output Directory

    Set outDir to "./dist" to keep compiled JavaScript files separate from your source. Set rootDir to "./src" so TypeScript only compiles files from your source folder. This keeps your project clean.

    Module System

    For Node.js, set module to "commonjs" and target to "ES2020" or higher. This ensures your compiled code uses Node’s native module resolution.

    Source Maps

    Enable sourceMap: true to generate .map files. Source maps let you debug the original TypeScript code directly in your browser or Node debugger, rather than the compiled JavaScript.

    Sample tsconfig.json

    {
      "compilerOptions": {
        "target": "ES2020",
        "module": "commonjs",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "outDir": "./dist",
        "rootDir": "./src",
        "sourceMap": true
      },
      "include": ["src"],
      "exclude": ["node_modules", "dist"]
    }

    Integrating with Node.js

    If you’re building a Node.js application, you need type definitions for Node’s built-in modules like fs and path. Install them with npm install --save-dev @types/node. Without these, TypeScript will complain about missing types when you use require('fs') or import * as fs from 'fs'.

    Now you can write TypeScript files that use Node APIs. For example, a simple HTTP server:

    import * as http from 'http';
    
    const server = http.createServer((req, res) => {
      res.writeHead(200, { 'Content-Type': 'text/plain' });
      res.end('Hello TypeScript!');
    });
    
    server.listen(3000, () => {
      console.log('Server running on port 3000');
    });

    To run this, compile first (npx tsc) and then run the output: node dist/index.js. Or you can use ts-node to run TypeScript directly in development: npm install --save-dev ts-node and then npx ts-node src/index.ts.

    Adding a Build Script and Watch Mode

    Your package.json should include a build script so you don’t type npx tsc every time. Add these scripts:

    "scripts": {
      "build": "tsc",
      "start": "node dist/index.js",
      "dev": "ts-node src/index.ts"
    }

    For a better development experience, use TypeScript’s watch mode. Run npx tsc --watch to automatically recompile whenever you save a file. You can also add a script: "watch": "tsc --watch". This pairs well with nodemon for Node projects — nodemon restarts your server when compiled files change.

    Common Pitfalls and Best Practices

    Watch out for these common mistakes when setting up TypeScript from scratch:

    • Missing @types/node: You’ll get errors like “Cannot find name ‘require’” or “Cannot find module ‘fs’”. Install the types and set types: ["node"] in tsconfig if you want explicit control.
    • Not excluding dist from compilation: If you do not exclude the dist folder, TypeScript may compile its own output again, causing infinite loops or duplicate declarations. Add "exclude": ["node_modules", "dist"].
    • Ignoring strict mode: Skipping strict mode might let sloppy code through. It’s easier to start strict and loosen only where needed than to tighten later.
    • Forgetting esModuleInterop: Without esModuleInterop: true, importing CommonJS modules can be awkward. Enable it for a more natural import syntax.
    • Mismatched rootDir and outDir: Ensure your rootDir includes all source files. If you have files outside src, they won’t be compiled unless you adjust the include patterns.
    • Using relative paths incorrectly: When you set outDir, the folder structure inside dist mirrors the source. If your imports include deep relative paths, they still work because the structure is preserved.

    Comparison: Default vs. Production-Ready Setup

    The table below highlights the differences between a minimal TypeScript setup and one you’d use for a real project.

    FeatureDefault npx tsc –initProduction-Ready Setup
    Structural typingstrict: falsestrict: true
    Output folderSame as source./dist
    Source mapsfalsetrue
    Node type definitionsNot included@types/node installed
    Watch modeNot configuredtsc –watch script

    Managing Multiple Configuration Files

    As your project grows, you might need different TypeScript configurations for different environments. For example, you may want a stricter production build and a more lenient development build. You can create multiple tsconfig files and extend a base config. Create a tsconfig.base.json with shared options, then tsconfig.json for development and tsconfig.prod.json for production. Use the extends field:

    // tsconfig.prod.json
    {
      "extends": "./tsconfig.base.json",
      "compilerOptions": {
        "outDir": "./dist",
        "sourceMap": false
      },
      "exclude": ["node_modules", "dist", "**/*.test.ts"]
    }

    Then run npx tsc -p tsconfig.prod.json to build with the production config. This pattern keeps your configurations DRY and maintainable.

    Next Steps: Linting and Formatting

    Once your TypeScript project compiles cleanly, consider adding ESLint and Prettier. ESLint with the @typescript-eslint plugin catches code-quality issues, while Prettier enforces consistent formatting. Install them as dev dependencies and create configuration files for each. Many teams also integrate Husky to run linting before commits.

    Your setup is now ready for development. Start writing code with confidence, knowing TypeScript has your back.

    Frequently asked questions

    What is the easiest way to set up TypeScript from scratch?

    The easiest way is to install TypeScript via npm, run npx tsc –init to create a tsconfig.json, then write a .ts file and compile it. For a Node.js project, also install @types/node. Use scripts in package.json to automate builds.

    Do I need to install TypeScript globally?

    It is recommended to install TypeScript as a dev dependency per project (npm install –save-dev typescript) rather than globally. This ensures consistent versions across your team and avoids version conflicts.

    How do I configure TypeScript for Node.js?

    Set module to commonjs, target to ES2020, enable esModuleInterop, and install @types/node. Also set strict to true, outDir to ./dist, and rootDir to ./src for a clean build output.

    What is the difference between tsc and ts-node?

    tsc compiles TypeScript to JavaScript, which you then run with Node. ts-node runs TypeScript directly without a separate compile step, making it convenient for development. Use tsc for production builds and ts-node for rapid iteration.

    How do I debug TypeScript code?

    Enable source maps in tsconfig (sourceMap: true). Then you can debug the original TypeScript files using Node’s inspector (node –inspect dist/index.js) or your editor’s debugger. Tools like Visual Studio Code support breakpoints in .ts files when source maps are present.

  • 10 Common Accessibility Mistakes Frontend Developers Make

    10 Common Accessibility Mistakes Frontend Developers Make

    Short answer: Common accessibility mistakes include insufficient color contrast, missing form labels, non-semantic HTML, poor keyboard navigation, images without alt text, and improper use of ARIA. These errors harm users with disabilities and can be fixed with testing and awareness.

    Key takeaways

    • Always use semantic HTML elements over divs for structure.
    • Provide clear labels and instructions for all form fields.
    • Ensure color contrast meets WCAG AA standards (4.5:1).
    • Test your app with keyboard-only navigation.
    • Add descriptive alt text to all meaningful images.
    • Use ARIA sparingly and only when native HTML is insufficient.

    Accessibility is often an afterthought in frontend development, but it shouldn’t be. Many developers unknowingly introduce barriers for users with disabilities. The good news? Most common accessibility mistakes are easy to fix once you know what to look for. This article covers the top 10 mistakes and how to avoid them.

    Person checking color contrast on a laptop screen
    Testing color contrast with a tool. — Photo: shogun / Pixabay

    1. Insufficient Color Contrast

    Color contrast is one of the most frequent accessibility issues. Text with low contrast against its background is hard to read, especially for users with low vision or color blindness. The Web Content Accessibility Guidelines (WCAG) require a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text.

    To fix this, use a contrast checking tool during design. Avoid light gray text on white backgrounds. Stick to dark text on light backgrounds or vice versa. If you use brand colors, verify they meet the ratio. Tools like the WebAIM Contrast Checker can help you test combinations.

    A common mistake is relying on color alone to convey information, like indicating required fields with only red text. Always supplement color with icons, patterns, or text labels. When designing, consider users with color blindness by checking your palette in a simulator. Also, ensure that hover and focus states maintain sufficient contrast — a subtle change may not be noticeable enough.

    2. Missing Form Labels

    Forms without proper labels are a nightmare for screen reader users. Never rely solely on placeholder text as a label. Placeholders disappear when users start typing, and they often have insufficient contrast.

    Every input should have an explicit <label> element with a for attribute matching the input’s id. Alternatively, you can use aria-label or aria-labelledby. Group related checkboxes and radio buttons with a <fieldset> and <legend>. For example:

    <label for="email">Email Address</label>
    <input type="email" id="email" name="email">

    If you cannot use a visible label for design reasons, still provide an accessible label with aria-label. But remember, visible labels help all users, not just those using screen readers. Also, ensure that error messages are associated with inputs using aria-describedby, so screen readers announce them.

    3. Non-Semantic HTML

    Using <div> and <span> for everything is common but harmful. These elements provide no semantic meaning to assistive technologies. Instead, use native HTML elements that carry built-in roles and behaviors.

    • Use <nav> for navigation
    • Use <main> for primary content
    • Use <button> for clickable actions
    • Use <heading> elements (h1-h6) for headings

    Semantic HTML is the foundation of accessible web development. It reduces the need for ARIA and makes your code more maintainable. For more on optimizing your frontend, check out React vs Next.js: Choosing the Right Framework for Your Project.

    A common misstep is using a <div> with an onclick for a button. While you can add ARIA roles and keyboard listeners, a native <button> is easier and more reliable. Similarly, using <h1> for branding instead of the page title misleads screen reader users. Ensure your heading hierarchy is logical and each page has exactly one <h1> that describes the page content.

    4. Poor Keyboard Navigation

    Not all users can use a mouse. Some rely on keyboard navigation. Common mistakes include missing focus indicators, incorrect tab order, and interactive elements that are not keyboard accessible.

    Always provide a visible focus outline (never use outline: none without a replacement). Ensure the tab order follows the visual order of the page. All interactive elements like links, buttons, and form controls should be reachable and operable via keyboard. Test your site by tabbing through every element.

    Also, avoid setting tabindex greater than 0, as it disrupts the natural focus order. Instead, organize your DOM flow to match visual order. For custom components like dropdowns or modals, trap focus within the modal when open and return focus to the trigger element on close. Use JavaScript to manage focus, but test thoroughly.

    5. Images Without Alt Text

    If an image has no alt text, screen readers may read the file name, which is useless. Informative images need descriptive alt text. Decorative images should have alt="" (empty alt) to be ignored by assistive technology.

    Write alt text that conveys the purpose of the image. For example, “A dog playing fetch in a park” is good for a photo of a dog. Don’t say “Image of a dog” — that’s redundant. Avoid keyword stuffing. If the image is a link, describe the destination. For complex images like charts, provide a text alternative nearby.

    A common mistake is writing overly long alt text. Keep it concise but informative. For SVG icons used as buttons, give them role="img" and aria-label. For background images in CSS, ensure the content is decorative, or include equivalent text in the HTML.

    6. Improper Use of ARIA

    ARIA (Accessible Rich Internet Applications) can enhance accessibility, but misuse can make things worse. A common mistake is adding role="button" to a <div> without adding keyboard support. Another is overusing ARIA attributes where native HTML would suffice.

    Follow the first rule of ARIA: don’t use ARIA if you can use a native HTML element. When you do use ARIA, test with a screen reader. Use roles, states, and properties correctly. For example, aria-expanded should reflect the actual state of a toggle.

    Another pitfall is using tabindex="-1" on non-interactive elements just to make them focusable via script. This can confuse users if not managed carefully. Only make elements focusable if they are interactive. Similarly, avoid using aria-hidden="true" on focusable elements, as it hides them from screen readers but not from keyboard focus.

    7. Not Providing Sufficient Focus Indicators

    Some developers remove focus outlines for aesthetic reasons, leaving keyboard users lost. A strong, visible focus indicator is essential. You can style it with :focus-visible to show it only when needed (e.g., keyboard navigation).

    Aim for a 2px solid outline with a color that contrasts well with the background. Do not rely solely on changes in background color, as that may not be noticeable.

    For custom components like tabs or menus, ensure focus indicators are visible on all states. If you use a box-shadow for focus, ensure it has sufficient contrast and is not clipped. Test your focus styles in high contrast mode on Windows, as some themes override outlines.

    Web form with text input and visible label
    Every form input needs a clear label. — Photo: ptdh / Pixabay

    8. Inaccessible Dynamic Content

    Single-page applications often update content without page reloads, which can confuse screen readers. When content changes dynamically (e.g., loading more items, updating a cart), you need to inform assistive technologies.

    Use aria-live regions to announce changes. Choose the right politeness setting: aria-live="polite" for non-critical updates, aria-live="assertive" for urgent ones. For example, when a user adds an item to a cart, announce “Item added to cart” via a live region. Also, move focus to newly added content when appropriate.

    A common mistake is using aria-live on the entire container that gets updated, which may read too much content. Instead, target a small region or use a dedicated live region for announcements. For infinite scroll, provide an accessible way to load more content, like a “Load more” button, and announce the number of new items.

    9. Missing Captions and Transcripts for Media

    Videos need captions for deaf and hard-of-hearing users. Audio content needs transcripts. This is not just a good practice — it’s required by WCAG at level A.

    Provide synchronized captions for videos. For audio podcasts, include a text transcript. Transcripts also benefit SEO and users who prefer reading. Tools like automatic captioning can generate a starting point, but always review for accuracy.

    For live media, strive for real-time captions. If that is not feasible, provide a summary or alternative. Also, ensure that video players themselves are accessible — controls should be keyboard operable and labeled with ARIA.

    10. Not Testing with Real Users or Tools

    Many developers rely only on automated checkers, but automation catches only about 30% of accessibility issues. Manual testing and real user feedback are essential.

    Use a combination of tools like axe, Lighthouse, and WAVE for automated checks. Then do manual keyboard testing and test with a screen reader (VoiceOver on Mac, NVDA on Windows). Involve users with disabilities in usability testing if possible. For more on improving site performance alongside accessibility, see Lazy Loading for Better Web Performance.

    11. Misusing Heading Hierarchy

    Headings are the backbone of page navigation for screen reader users. A common mistake is skipping heading levels, like jumping from an h2 to an h4, or using headings purely for visual styling. This breaks the logical structure and confuses users.

    Always maintain a logical heading hierarchy: h1 for the page title, h2 for main sections, h3 for subsections, and so on. Never skip levels — if you need a smaller visual size, use CSS instead of a lower heading level. Use browser extensions like the HeadingsMap tool to verify your structure.

    12. Ignoring Motion and Time-Based Interactions

    Animations, auto-playing videos, and time limits can be problematic for users with vestibular disorders, ADHD, or cognitive disabilities. A common mistake is not providing controls to pause, stop, or hide moving content.

    Offer a way to reduce motion for users who prefer it, using the prefers-reduced-motion media query. For auto-playing videos, include a pause button prominently. For time limits, allow users to extend or disable them. If you use scrolling text or carousels, provide play/pause controls and avoid infinite auto-scroll.

    How to Get Started with Fixing Accessibility

    Start by auditing your existing site. Run an automated scan, fix critical issues like contrast and form labels, then test manually. Accessibility is an ongoing process, not a one-time fix.

    Educate your team on a11y best practices. Add accessibility checks to your CI pipeline. Make it part of your definition of done. Small changes add up to a much better experience for everyone.

    For more on performance and accessibility, read How to Improve Web Performance with Lazy Loading.

    Frequently asked questions

    What is the most common accessibility mistake?

    Insufficient color contrast is one of the most common mistakes. Low-contrast text is hard to read for many users. Always ensure text meets WCAG AA contrast ratios of at least 4.5:1 for normal text and 3:1 for large text.

    How can I test my site for accessibility?

    Use automated tools like axe or Lighthouse for initial checks. Then manually test with keyboard navigation and a screen reader (e.g., VoiceOver on Mac, NVDA on Windows). Involve users with disabilities for the most thorough evaluation.

    Do I need to add ARIA to every element?

    No. Use native HTML elements first, as they have built-in semantics. ARIA should only augment HTML when native elements cannot provide the needed accessibility. Overusing ARIA can actually harm accessibility.

    What should I put in alt text?

    Alt text should describe the purpose or content of the image. For informative images, be concise but descriptive. For decorative images, use empty alt (alt=””). Avoid phrases like ‘image of’ and never stuff keywords.

    Can accessibility improvements also help SEO?

    Yes, many accessibility practices like semantic HTML, descriptive alt text, and good color contrast also benefit SEO. Search engines favor well-structured, readable content. Accessibility and SEO often go hand in hand.

  • React vs Next.js: Choosing the Right Framework for Your Project

    React vs Next.js: Choosing the Right Framework for Your Project

    Short answer: React is a library for building user interfaces, best for single-page applications and when you need full control over the stack. Next.js is a React framework that adds server-side rendering, static site generation, and routing out of the box. Use React for interactive SPAs; choose Next.js for SEO-friendly, content-heavy, or multi-page sites.

    Key takeaways

    • React is ideal for highly interactive single-page applications.
    • Next.js provides SSR and SSG for better SEO and performance.
    • Next.js includes built-in routing, image optimization, and API routes.
    • React gives you more flexibility but requires more setup.
    • Choose Next.js for content-driven sites and e-commerce.
    • React is lighter for simple apps with a custom backend.

    Every developer faces the decision: React or Next.js? Both are popular in the frontend world, but they serve different purposes. React is a JavaScript library for building user interfaces. Next.js is a full framework built on top of React. This guide will help you understand when to use each one for your project.

    Developer writing React code on a laptop with code editor open
    Building a React application — Photo: Pexels / Pixabay

    What is React?

    React, maintained by Meta, is a library for building user interfaces. It lets you create reusable UI components and manage state efficiently. React uses a virtual DOM to update only the parts of the page that change, making it fast for dynamic interactions.

    You can use React with other tools like Webpack, Babel, and React Router to build a complete application. But out of the box, React handles only the view layer. You have to choose your own tools for routing, data fetching, and build configuration.

    What is Next.js?

    Next.js is a React framework created by Vercel. It provides a complete solution for building React applications. It includes built-in routing, server-side rendering (SSR), static site generation (SSG), API routes, and image optimization. Next.js also supports incremental static regeneration and middleware.

    With Next.js, you get a lot of decisions made for you. This speeds up development and enforces best practices. You can still customize things, but the defaults are production-ready.

    Key Differences Between React and Next.js

    Let’s break down the major differences in a comparison table.

    Feature React Next.js
    Type Library Framework
    Rendering Client-side only (unless you add a third-party) SSR, SSG, CSR, ISR
    Routing Manual (React Router) File-based routing
    SEO Poor without SSR Excellent with SSR/SSG
    Performance Good for SPAs Better initial load time
    Data Fetching Any library (fetch, Axios) getServerSideProps, getStaticProps
    API Routes Not included Built-in
    Image Optimization Not included Built-in component
    Bundle Size Smaller (only React) Larger (framework features)
    Learning Curve Moderate Moderate, plus specific Next.js concepts

    This table shows that Next.js extends React with many features. The choice depends on whether you need those features or prefer the simplicity of React alone.

    When to Choose React

    React is a great choice for single-page applications (SPAs) that require heavy user interaction. For example, a dashboard for analytics, a project management tool, or a social media feed. These apps work well with client-side rendering because they change data often without full page reloads.

    Consider React when you need full control over your tech stack. You want to pick your own state management (Redux, Zustand, etc.), routing library, and build tools. React also integrates easily with Micro Frontend architectures where each team manages its own part of the UI.

    If your application has a backend that handles SEO, like an already server-rendered page that includes a React widget, then React alone is sufficient. Also, if you are building a mobile app with React Native, sharing components with a web version is simpler if you stick to plain React.

    Another case is when server-side rendering is not necessary. For example, an internal admin panel or a logged-in user portal where search engines don’t index the content. In such cases, React’s client-side rendering is fast enough, especially if you improve web performance with lazy loading of components and data.

    When to Choose Next.js

    Next.js excels for content-driven websites where SEO matters. Blogs, e-commerce stores, marketing sites, and documentation portals benefit from server-side rendering or static generation. Search engines can read the full HTML content, improving rankings.

    Use Next.js when you need fast initial page loads. With SSR, the server sends a fully rendered page, so users see content immediately. With SSG, you can pre-render pages at build time and serve them from a CDN, which is incredibly fast.

    Next.js also shines for applications that need both static and dynamic content. You can combine static generation for public pages and server-side rendering for user-specific pages. The file-based routing simplifies navigation, and API routes let you build a backend without a separate server.

    If you are building a multi-page application with complex routing, Next.js saves time. The built-in support for layouts and dynamic routes makes development more straightforward. Additionally, features like automatic image optimization and code splitting boost performance out of the box.

    For large projects with many developers, Next.js enforces a consistent structure. The pages directory (or app directory in newer versions) defines routes clearly. This reduces decision fatigue and helps new team members onboard faster.

    Developers collaborating on a Next.js project with sticky notes and whiteboard
    Team developing with Next.js — Photo: truthseeker08 / Pixabay

    Performance Considerations

    Performance is a key factor when choosing between React and Next.js. React applications start with an empty HTML shell and load JavaScript to render content. This can lead to slower initial load times, especially on slow networks. However, once loaded, React apps can feel extremely responsive because only necessary components re-render.

    Next.js improves performance with server-side rendering. The server sends HTML that the browser can display immediately. Then React hydrates to make it interactive. For static pages, Next.js generates HTML at build time, so the server does minimal work per request. This pattern also helps with lazy loading for better web performance of images and scripts.

    Another aspect is bundle size. A plain React app with no extra libraries is smaller than a Next.js app. But Next.js automatically code-splits per route, so each page only loads what it needs. In practice, Next.js often wins on overall performance because users see content faster.

    Development Experience and Learning Curve

    Getting started with React is straightforward if you use Create React App. You write components and manage state. But as your project grows, you need to add routing, state management, and build configurations. This can become complex.

    Next.js provides a more opinionated setup. You follow its file-based routing and use its data fetching methods. The learning curve includes understanding SSR, SSG, and when to use each. However, once you learn these concepts, development becomes more predictable. The official documentation is excellent and includes step-by-step tutorials.

    If you are new to React, starting with Next.js can be beneficial because it teaches you patterns like SSR and static generation early. However, if you want to deeply understand React without abstractions, start with plain React.

    When to Combine Both

    You don’t have to choose one exclusively. Some projects use React for micro-frontends and Next.js for entry points. For example, a large e-commerce site might have a Next.js frontend for product pages (SEO) and an admin panel built with plain React. This gives you the best of both worlds.

    Also, you can embed React components inside a Next.js page. Next.js is fully compatible with React, so you can use any React library. Actually, Next.js is React under the hood. So switching from React to Next.js is often a gradual migration.

    Making the Decision

    The decision comes down to your project requirements. If you need a simple single-page app with no SEO concerns, go with React. If you need SEO, fast initial load, or a multi-page application, choose Next.js. Consider your team’s experience and the time you have for setup.

    Both are backed by strong communities and are excellent choices. The right framework is the one that solves your problems without adding unnecessary complexity. Start small, experiment, and learn from real projects. And if you run into layout issues, check out common CSS Grid mistakes and how to fix them for help.

    Whichever you choose, focus on writing clean components and managing state well. The framework is just a tool. Your code quality matters more.

    Frequently asked questions

    Can I use React and Next.js together in the same project?

    Yes, Next.js is built on top of React, so all React components work inside Next.js. You can gradually migrate an existing React app to Next.js or embed React widgets in a Next.js page. They are fully compatible.

    Which one is better for SEO, React or Next.js?

    Next.js is better for SEO because it supports server-side rendering and static site generation. These techniques send fully rendered HTML to search engine crawlers. React alone relies on client-side rendering, which search engines may not index as effectively.

    Is Next.js harder to learn than React?

    Next.js adds concepts like SSR, SSG, and file-based routing on top of React. If you already know React, learning Next.js is not very hard. For beginners, starting with React first might be simpler, but Next.js documentation is clear and helps you build complete applications quickly.

    Does Next.js replace React?

    No, Next.js is a framework that uses React as the view library. It does not replace React. You still write React components and use React hooks. Next.js adds extra features like routing, rendering modes, and API routes on top of React.

    Can I deploy a Next.js app without a server?

    Yes, Next.js supports static site generation (SSG), which produces static HTML files. You can deploy these files to any static hosting like Vercel, Netlify, or an S3 bucket. For dynamic features like SSR, you need a Node.js server, but Vercel handles that well.

  • Lazy Loading for Better Web Performance

    Lazy Loading for Better Web Performance

    Short answer: Lazy loading delays loading of offscreen images, videos, and iframes until the user scrolls near them. It reduces initial page weight, speeds up load times, and saves bandwidth. Modern browsers support native lazy loading via the loading=’lazy’ attribute on img and iframe elements.

    Key takeaways

    • Native lazy loading with loading=’lazy’ is the simplest implementation.
    • Intersection Observer offers more control for custom lazy loading.
    • Always include width and height attributes to prevent layout shifts.
    • Lazy load images below the fold; keep above-the-fold images eager.
    • Apply lazy loading to videos, iframes, and background images too.
    • Measure performance gains using Lighthouse or WebPageTest.

    Imagine landing on a web page that feels instant. Content appears as you scroll, images load just in time, and you never stare at a blank white box. That’s the promise of lazy loading—a technique that defers loading non-visible resources until the user needs them. For frontend developers, implementing lazy loading is one of the most impactful ways to improve web performance without a massive overhaul.

    Developer writing lazy loading code on a laptop
    Implementing lazy loading with native attributes or JavaScript. — Photo: Innovalabs / Pixabay

    What Is Lazy Loading and Why Does It Matter?

    Lazy loading means postponing the loading of resources that are not immediately needed. Instead of downloading every image, video, or iframe when the page first loads, the browser only loads what’s visible above the fold. The rest are fetched as the user scrolls down.

    Why does this matter? Because download size directly affects load time. A typical webpage today weighs over 2 MB, with images accounting for the largest share. By deferring offscreen images, you can reduce initial page weight by 30% or more. This leads to faster Time to Interactive, better Largest Contentful Paint (LCP), and lower data usage for mobile users.

    User expectations are high: they expect pages to load in under two seconds. Lazy loading helps you meet that benchmark without compromising on visual richness.

    Native Lazy Loading: The Easiest Way to Start

    The simplest method is native lazy loading using the loading attribute. Add loading="lazy" to your <img> and <iframe> tags, and the browser handles the rest. It’s supported in all modern browsers since 2020.

    <img src="large-photo.jpg" alt="A scenic mountain landscape" loading="lazy" width="800" height="600">

    Always include width and height attributes. This prevents Cumulative Layout Shift (CLS) because the browser reserves the space before the image loads. Without explicit dimensions, lazy loaded images can cause layout jumps when they finally appear.

    For iframes, the approach is identical:

    <iframe src="https://example.com/map" loading="lazy" width="600" height="450"></iframe>

    Native lazy loading works well for most use cases. However, it gives you limited control. If you need to add custom effects like fading in images or tracking load events, you’ll want JavaScript-based solutions.

    Intersection Observer: Custom Lazy Loading with JavaScript

    The Intersection Observer API lets you efficiently detect when an element enters the viewport. It’s more powerful than older scroll-event-based approaches because it doesn’t cause layout thrashing.

    Basic Implementation

    Here’s a minimal example that lazy loads images using Intersection Observer:

    const images = document.querySelectorAll('img[data-src]');
    
    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const img = entry.target;
                img.src = img.dataset.src;
                img.removeAttribute('data-src');
                observer.unobserve(img);
            }
        });
    });
    
    images.forEach(img => observer.observe(img));

    Use data-src to store the actual image URL. When the image enters the viewport, swap it into the src attribute and stop observing it to free memory.

    Adding a Blur-Up or Placeholder Effect

    A common enhancement is to show a tiny, blurred placeholder while the full image loads. Use two images: a tiny compressed version (e.g., 20px wide) as a background or separate <img>, and lazy load the full-resolution one on top. The placeholder loads instantly, and the crisp image fades in later.

    Here’s a practical implementation using CSS and Intersection Observer:

    <div class="lazy-image-wrapper">
        <img class="placeholder" src="tiny-blur.jpg" alt="">
        <img class="lazy" data-src="full-image.jpg" alt="Description" width="800" height="600">
    </div>
    
    <style>
    .lazy-image-wrapper { position: relative; overflow: hidden; }
    .lazy-image-wrapper img { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover; }
    .placeholder { filter: blur(10px); transform: scale(1.1); }
    .lazy { opacity: 0; transition: opacity 0.3s; }
    .lazy.loaded { opacity: 1; }
    </style>
    
    <script>
    const lazyImages = document.querySelectorAll('.lazy');
    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const img = entry.target;
                img.src = img.dataset.src;
                img.onload = () => img.classList.add('loaded');
                observer.unobserve(img);
            }
        });
    }, { rootMargin: '200px' });
    lazyImages.forEach(img => observer.observe(img));
    </script>

    The placeholder image should be extremely small in file size (under 1KB). The effect creates a smooth visual transition and improves perceived performance.

    When to Use Intersection Observer Over Native

    Use the Intersection Observer when you need: custom loading animations, load event callbacks, lazy loading of background images, or support for older browser versions (though native is now widely supported).

    Web performance dashboard showing improved metrics after lazy loading
    Measure the impact of lazy loading on your performance scores. — Photo: JillWellington / Pixabay

    Lazy Loading Videos and Iframes

    Lazy loading isn’t just for images. Videos and third-party embeds (like YouTube or maps) can also be deferred.

    Video Lazy Loading

    For HTML video elements, set preload="none" and use a poster image. Then, use Intersection Observer to start loading the video when the user scrolls near it. Avoid autoplaying videos with sound; they hurt performance and user experience.

    <video preload="none" poster="thumbnail.jpg" width="640" height="360">
        <source src="video.mp4" type="video/mp4">
    </video>

    One common mistake with video lazy loading is not handling the source element correctly. If you dynamically set the src on the video element, ensure you also trigger loading by calling video.load() after assigning the source. Otherwise, the browser may not fetch the video.

    Iframe Lazy Loading

    Third-party widgets like social media feeds or maps often contain heavy JavaScript. Defer them with loading="lazy" on the iframe, or for older browsers, add the src attribute only when the element comes into view.

    For YouTube embeds, a clean technique is to replace the iframe with a clickable thumbnail image. When the user clicks, you swap in the real iframe and start playback. This reduces initial page weight significantly and only loads the YouTube player on demand.

    <div class="youtube-placeholder" data-embed="dQw4w9WgXcQ">
        <img src="thumbnail.jpg" alt="Video title" width="560" height="315">
        <button class="play-button">Play</button>
    </div>
    
    <script>
    document.querySelectorAll('.youtube-placeholder').forEach(placeholder => {
        placeholder.addEventListener('click', () => {
            const iframe = document.createElement('iframe');
            iframe.src = `https://www.youtube.com/embed/${placeholder.dataset.embed}?autoplay=1`;
            iframe.width = 560;
            iframe.height = 315;
            iframe.allow = 'autoplay';
            placeholder.innerHTML = '';
            placeholder.appendChild(iframe);
        });
    });
    </script>

    This approach also avoids the performance hit of loading the YouTube player for every visitor, even those who never play the video.

    Lazy Loading Background Images

    CSS background images are not covered by the native loading attribute. To lazy load them, you need JavaScript. A common technique is to set a data-background attribute on elements and swap it in using Intersection Observer:

    <div class="hero" data-background="url('hero.jpg')"></div>
    
    <script>
    const bgElements = document.querySelectorAll('[data-background]');
    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const el = entry.target;
                el.style.backgroundImage = el.dataset.background;
                el.removeAttribute('data-background');
                observer.unobserve(el);
            }
        });
    });
    bgElements.forEach(el => observer.observe(el));
    </script>

    One thing to watch out for: background images can cause layout shifts if their container has no intrinsic dimensions. Always set explicit heights or aspect ratios on these containers to reserve space.

    Comparison: Native vs. Intersection Observer

    Feature Native Lazy Loading Intersection Observer
    Browser Support Modern browsers only Polyfillable, wider support
    Ease of Implementation Very easy – one attribute Moderate – requires JS
    Customization None Full control
    Performance Excellent, browser-optimized Good, but slight overhead
    Built-in Layout Shift Prevention Needs explicit dimensions Needs explicit dimensions
    Background Images Not supported Works with JavaScript
    Event Tracking No load events Can track when loaded

    For most projects, start with native lazy loading and only add Intersection Observer if you need advanced features.

    Best Practices and Common Pitfalls

    Always Specify Dimensions

    Lazy loading without width and height leads to layout shifts. Modern CSS can help with responsive images: use aspect-ratio or max-width: 100%; height: auto; on images that have explicit width and height attributes. For containers, set aspect-ratio to match the content dimensions to reserve space.

    Don’t Lazy Load Above-the-Fold Content

    Critical images (hero banners, logos, primary content) should load eagerly. Add loading="eager" or simply omit the attribute. Lazy loading above-the-fold images can delay LCP and hurt perceived performance. A good rule of thumb: if an image is visible on initial load, load it normally.

    Set Appropriate Thresholds

    With Intersection Observer, set a reasonable rootMargin to start loading images before they enter the viewport (e.g., rootMargin: '200px'). This ensures images are ready when the user scrolls. Be careful not to set too large a margin—loading everything too early defeats the purpose of lazy loading.

    Test with Real Users and Tools

    Use Lighthouse, WebPageTest, or Chrome DevTools to verify that lazy loading is working correctly. Check that images are not deferred unnecessarily and that no visible content flickers or shows empty space. Pay attention to the LCP element: it should not be lazy loaded.

    Handle JavaScript Disabled Users

    If you rely on JavaScript for lazy loading, provide fallback content (like a <noscript> tag with the full image) so users without JavaScript still see the content. For background images, consider using a default background that works without JS.

    Performance Gains: What to Expect

    Lazy loading can reduce initial page weight significantly. In a typical blog with many images, you might see Time to Interactive drop by 20-30%. Bandwidth usage decreases, especially on mobile networks. However, lazy loading does not improve perceived performance if not implemented correctly—test both with and without it.

    Monitor key metrics: LCP should remain under 2.5 seconds, CLS below 0.1, and First Contentful Paint under 1.8 seconds. Lazy loading primarily affects LCP and initial load time, not CLS (if you use dimensions) or FCP.

    Testing Your Lazy Loading Implementation

    Before shipping, test on a throttled network (e.g., Fast 3G in Chrome DevTools) and verify that images load as you scroll. Check the Network panel to confirm that offscreen images are not requested until they come into view. Also, ensure that the placeholder images appear immediately and that there are no blank gaps.

    One common issue: if you use loading="lazy" on images that are above the fold, you might see a delay in LCP. Use Performance panel to identify these cases and remove lazy loading from critical images.

    Another test: load the page with JavaScript disabled. If you used Intersection Observer, users should still see content via <noscript> fallbacks. Without fallbacks, you risk blank pages for a non-trivial portion of users.

    Implementing lazy loading is a practical step you can take today. Start with native attributes for images and iframes, then progressively enhance with Intersection Observer for custom behaviors. Your users will notice faster load times, and your performance scores will thank you.

    Frequently asked questions

    Does lazy loading affect SEO?

    Lazy loading itself does not hurt SEO if implemented correctly. Search engines like Google can discover lazy loaded images as long as they are in the HTML markup (e.g., using data-src) and the page is rendered properly. Avoid hiding content from crawlers by using proper fallbacks or server-side rendering.

    Can I lazy load background images in CSS?

    Yes, you can lazy load CSS background images using JavaScript. One approach is to initially set the background-image to none, then apply the actual image URL via Intersection Observer when the element enters the viewport. Native lazy loading does not apply to CSS background images.

    Is native lazy loading supported in all browsers?

    Native lazy loading is supported in all major modern browsers: Chrome, Firefox, Edge, and Safari since 2020. Internet Explorer and older versions of Safari do not support it. For those, you can use a polyfill or fall back to Intersection Observer.

    What is the difference between lazy loading and deferred loading?

    Lazy loading defers resources until they are needed (usually when the user scrolls near them), while deferred loading delays resource loading until after the page has loaded, often used for JavaScript scripts. Deferred scripts still load eventually, but lazy loading only triggers on demand.

    Should I lazy load images inside a carousel or slider?

    Yes, lazy loading images inside carousels is recommended if the carousel has many slides that the user may not see. Load only the first few slides eagerly, then lazy load the rest as the user navigates. This reduces initial payload and improves performance.

  • How to Improve Web Performance with Lazy Loading

    How to Improve Web Performance with Lazy Loading

    Short answer: Lazy loading delays loading of non-critical resources until they are needed, typically when they enter the viewport. It reduces initial page weight, lowers bandwidth usage, and improves key metrics like Largest Contentful Paint (LCP) and First Input Delay (FID). Native lazy loading via the loading=”lazy” attribute is the simplest method for images and iframes, while Intersection Observer works for custom elements.

    Key takeaways

    • Lazy loading reduces initial page weight and improves load times.
    • Use native loading=”lazy” for images and iframes whenever possible.
    • Intersection Observer offers more control for custom lazy loading.
    • Always include width and height attributes to prevent layout shifts.
    • Lazy load below-the-fold content first; eager-load critical images.
    • Test with real users using Lighthouse and Web Vitals.

    Slow page loads frustrate users and hurt your search rankings. One of the most effective ways to speed up your site is lazy loading — deferring resources that aren’t immediately visible until they’re about to enter the viewport. This cuts down on initial data transfer, reduces CPU work, and makes your pages feel snappy. In this article, you’ll learn how lazy loading works, how to implement it with modern browser features and JavaScript, and what pitfalls to avoid.

    What Is Lazy Loading and Why Does It Matter?

    Lazy loading is a strategy that postpones the loading of non-critical resources — like images deep down the page, third-party embeds, or heavy scripts — until the moment they’re needed. Instead of downloading everything upfront, the browser loads only what’s in the initial viewport. The rest is loaded on demand.

    Why does this matter for performance? For starters, it reduces the total bytes transferred on the initial load. That directly improves your Largest Contentful Paint (LCP) because the main content isn’t competing with dozens of off-screen images. It also reduces CPU idle time and network congestion, which can lower your First Input Delay (FID) and Cumulative Layout Shift (CLS). Tools like Google Lighthouse heavily reward lazy loading for images and iframes.

    Native Lazy Loading: The Simplest Approach

    Developer writing JavaScript code for Intersection Observer lazy loading implementation
    Intersection Observer provides fine-grained control over lazy loading. — Photo: Boskampi / Pixabay

    The easiest way to lazy load images and iframes is to use the loading attribute. Adding loading="lazy" to an <img> or <iframe> tells the browser to defer loading until the element is close to the viewport. No JavaScript required.

    <img src="hero.jpg" alt="Hero image" loading="lazy" width="1200" height="800">

    For browsers that support it, native lazy loading is fast, efficient, and respects user preferences like data-saver mode. Always include explicit width and height attributes to prevent layout shifts while the image loads. If a browser doesn’t support loading, the image loads eagerly by default, so it’s safe to use today.

    When to Avoid Native Lazy Loading

    Critical above-the-fold images should never be lazy loaded. Doing so can hurt LCP because the browser may delay loading the hero image. Use loading="eager" or omit the attribute entirely for your most important visuals. A good rule of thumb is to lazy load any image that appears below the initial viewport.

    Using Intersection Observer for Custom Lazy Loading

    Bar chart comparing page load times before and after lazy loading implementation
    Measuring performance gains after implementing lazy loading. — Photo: Pexels / Pixabay

    Native lazy loading works great for images and iframes, but for other resources — like background images, custom elements, or lazy-loading a <picture> with multiple sources — you’ll want the Intersection Observer API. It provides a performant way to detect when an element enters the viewport.

    Here’s a basic example that replaces a placeholder data-src with the real src:

    const lazyImages = document.querySelectorAll('img[data-src]');
    
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target;
          img.src = img.dataset.src;
          img.onload = () => img.classList.add('loaded');
          observer.unobserve(img);
        }
      });
    }, { rootMargin: '200px 0px' });
    
    lazyImages.forEach(img => observer.observe(img));

    The rootMargin option triggers loading 200px before the element enters the viewport, which gives a smoother user experience. You can adjust this based on your layout.

    Lazy Loading Background Images

    Background images set via CSS can also be lazy loaded. Store the background URL in a data-bg attribute and apply it when the element becomes visible:

    const lazyBg = document.querySelectorAll('[data-bg]');
    
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const el = entry.target;
          el.style.backgroundImage = `url(${el.dataset.bg})`;
          observer.unobserve(el);
        }
      });
    });
    
    lazyBg.forEach(el => observer.observe(el));

    This technique works well for hero sections that appear further down the page or for card backgrounds in a long list.

    Lazy Loading JavaScript and Third-Party Embeds

    Not just images — you can lazy load scripts too. Heavy third-party libraries, analytics, maps, or social media embeds can block the main thread. Deferring them until after the page is interactive improves TBT (Total Blocking Time) and FID.

    One common pattern is to use <script defer> for non-critical scripts, but for truly lazy-loaded scripts, you can create them dynamically when needed:

    function loadScript(src) {
      const script = document.createElement('script');
      script.src = src;
      script.defer = true;
      document.body.appendChild(script);
    }
    
    // Load when user scrolls to a certain section
    window.addEventListener('scroll', () => {
      if (/* condition met */) {
        loadScript('https://example.com/widget.js');
      }
    }, { once: true });

    For iframes, native loading="lazy" works well, but you can also use Intersection Observer to swap placeholder content. YouTube embeds can be replaced with a static thumbnail and only load the full player on click.

    Best Practices and Common Mistakes

    Lazy loading isn’t a magic bullet. Done wrong, it can hurt performance and user experience. Here are some guidelines:

    • Always set width and height on images and iframes to reserve space and prevent CLS.
    • Lazy load below-the-fold content only. Critical images should load eagerly.
    • Avoid lazy loading everything. If an image is not visible on any screen size (e.g., hidden behind a tab), consider not loading it at all.
    • Test with real users. Use Lighthouse, Web Vitals, and real user monitoring (RUM) to ensure lazy loading isn’t delaying important content.
    • Provide good placeholders. Low-quality image placeholders (LQIP) or blur-up techniques improve perceived performance.

    One common mistake is lazy loading a hero image because it reduces LCP — but it actually increases it. Another is forgetting to update loading attributes when you change layout. Always review which images are above the fold on different devices.

    Comparison: Native vs. Intersection Observer

    Feature Native Lazy Loading Intersection Observer
    Browser Support Modern browsers (Chrome, Firefox, Edge, Safari 15.4+) Wide support in all modern browsers
    Implementation Effort Minimal – just add attribute Moderate – requires JavaScript
    Customization Limited (root margin via loading=”lazy” only) Full control via options and callbacks
    Use Cases Images and iframes Any resource (backgrounds, scripts, custom elements)
    Performance Browser-optimized, no JavaScript overhead Slightly more overhead but still performant

    Choose native lazy loading whenever you can. It’s simpler and faster. Reserve Intersection Observer for cases where native doesn’t cover your needs.

    Lazy Loading in Responsive Images

    When using the <picture> element with multiple sources, native lazy loading applies to the entire element. But you might want finer control. For example, you can combine Intersection Observer with the sizes attribute to load only the appropriate image for the viewport. Store different resolutions in data-srcset and switch them when the element is near the viewport. This avoids loading a desktop-sized image on mobile.

    <picture>
      <source media="(min-width: 800px)" data-srcset="large.jpg">
      <img data-src="small.jpg" alt="Example" width="800" height="600">
    </picture>

    In your observer, swap both srcset on the source elements and src on the img. This ensures you get the right image size for the device while still lazy loading.

    Handling Edge Cases: Print, Accessibility, and Caching

    Lazy loading can interfere with printing if images haven’t loaded yet. Add a CSS print rule to force all lazy images to become eager when printing:

    @media print {
      img[loading="lazy"], [data-src] {
        content: attr(data-src);
        /* force eager loading via JS */
      }
    }

    You can also use JavaScript to load all lazy resources when the user invokes print via window.beforeprint event. For accessibility, ensure that screen readers can still navigate lazy-loaded content. The loading attribute is accessible, but if you use JavaScript, make sure the resources are loaded when the focus reaches them, perhaps by observing focus events or using a preload buffer.

    Caching is another consideration. Browsers cache lazy-loaded images after they load, so subsequent visits or scrolls won’t re-fetch them. But if you lazy load scripts, ensure they have proper cache headers to avoid blocking future loads. Test with browser DevTools on throttled connections to verify that lazy loaded resources appear quickly when needed.

    Measuring the Impact

    After implementing lazy loading, measure the difference. Run Lighthouse before and after, and look at these metrics:

    • LCP: Should improve if critical images load faster without competition.
    • Total Image Bytes: Reduced on initial load.
    • Number of Requests: Fewer requests upfront.
    • Time to Interactive: Should decrease as less JavaScript is parsed.

    Also monitor real-user metrics like FID or INP. If you lazy load too aggressively, users may experience delays when scrolling. Adjust your rootMargin to trigger loads sooner if needed.

    For more on building performant layouts, check out our guide on Common CSS Grid Mistakes and How to Fix Them. Proper layout techniques complement lazy loading by reducing layout shifts.

    Conclusion: Start Small, Measure Often

    Lazy loading is a practical, high-impact performance optimization. Begin with native lazy loading on all images and iframes below the fold. Then explore Intersection Observer for more complex cases like background images or third-party scripts. Always measure before and after with Lighthouse and real-user monitoring. Avoid lazy loading critical above-the-fold assets, and always set dimensions to prevent layout shifts. With these techniques, you’ll deliver faster, smoother experiences for your users.

    Frequently asked questions

    What is lazy loading in web performance?

    Lazy loading is a technique that defers the loading of non-critical resources until they are needed, typically when they scroll into the viewport. It reduces initial page weight, speeds up load time, and improves performance metrics like LCP and FID.

    How do you lazy load images in HTML?

    You can lazy load images by adding the loading=’lazy’ attribute to the img tag. Browsers that support the attribute will defer loading until the image is close to the viewport. Always include width and height attributes to prevent layout shifts.

    Should I lazy load above-the-fold images?

    No, you should not lazy load critical above-the-fold images. Doing so can delay the Largest Contentful Paint (LCP). Eager-load hero images and other visible content, and lazy load only images below the initial viewport.

    What is the difference between native lazy loading and Intersection Observer?

    Native lazy loading uses the HTML loading attribute and is supported by modern browsers with zero JavaScript overhead. Intersection Observer is a JavaScript API that provides more control and works for any element, including background images and scripts, but requires more code.

    Does lazy loading affect SEO?

    Lazy loading can positively affect SEO by improving page speed and user experience, which are ranking factors. However, ensure that content is still accessible to search engine crawlers. Native lazy loading is generally safe, and you should avoid hiding important content behind lazy loading that isn’t triggered by crawlers.