Page Rendering Optimization (Virtual DOM, Virtual Scroll)

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1212
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815

Page Rendering Optimization (Virtual DOM, Virtual Scroll)

Slow rendering manifests specifically: a list of 10,000 rows hangs the browser when scrolling, a dashboard with charts lags when updating data, a modal opens with delay. Causes are always measurable — unnecessary re-renders, DOM with thousands of nodes, synchronous computations in render path. Optimization starts with profiling, not guessing.

Measurement Tools

Before any optimization — establish baseline:

React DevTools Profiler — record an interaction session, find components with high render duration and frequent re-renders. Pay special attention to why did this render — shows what changed.

Chrome PerformanceCtrl+Shift+PStart profiling and reload page. In flame chart look for long tasks (>50ms), Recalculate Style, Layout thrashing.

// Quick check without DevTools
const start = performance.now();
// ... operation
console.log(`Took: ${performance.now() - start}ms`);

// For components — React Profiler API
import { Profiler } from 'react';

<Profiler id="ProductList" onRender={(id, phase, actualDuration) => {
  if (actualDuration > 16) {
    console.warn(`Slow render: ${id} took ${actualDuration}ms (${phase})`);
  }
}}>
  <ProductList />
</Profiler>

Virtual Scroll: Why DOM Kills Performance

1000 rows in a table = 1000 DOM nodes (plus cells). Browser keeps everything in memory, recalculates styles on any change, scroll handler iterates over all elements. At 5000+ rows, page starts lagging on any hardware.

Virtualization renders only visible rows + small overscan. On scroll — replaces content, keeping one set of DOM nodes.

TanStack Virtual (formerly react-virtual)

Headless — doesn't dictate styles, works with any CSS:

npm install @tanstack/react-virtual
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';

interface VirtualListProps<T> {
  items: T[];
  itemHeight: number;
  renderItem: (item: T, index: number) => React.ReactNode;
}

function VirtualList<T>({ items, itemHeight, renderItem }: VirtualListProps<T>) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => itemHeight,
    overscan: 5,  // how many rows to render outside visible area
  });

  return (
    <div
      ref={parentRef}
      style={{ height: '600px', overflow: 'auto' }}
    >
      {/* Container with full height — for proper scrollbar */}
      <div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
        {virtualizer.getVirtualItems().map((virtualItem) => (
          <div
            key={virtualItem.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualItem.size}px`,
              transform: `translateY(${virtualItem.start}px)`,
            }}
          >
            {renderItem(items[virtualItem.index], virtualItem.index)}
          </div>
        ))}
      </div>
    </div>
  );
}

For variable-height rows — measureElement:

const virtualizer = useVirtualizer({
  count: items.length,
  getScrollElement: () => parentRef.current,
  estimateSize: () => 80,  // initial estimate
  measureElement: (el) => el.getBoundingClientRect().height,  // actual size
});

// In render row:
<div
  ref={virtualizer.measureElement}
  data-index={virtualItem.index}
>
  {/* variable content */}
</div>

React Window for Tables

For tables with fixed row sizes react-window is simpler:

npm install react-window react-window-infinite-loader
npm install --save-dev @types/react-window
import { FixedSizeList } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';

function VirtualTable({ data }: { data: Row[] }) {
  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
    <div style={style} className={`row ${index % 2 === 0 ? 'row--even' : ''}`}>
      <div className="cell">{data[index].id}</div>
      <div className="cell">{data[index].name}</div>
      <div className="cell">{data[index].status}</div>
    </div>
  );

  return (
    <AutoSizer>
      {({ height, width }) => (
        <FixedSizeList
          height={height}
          itemCount={data.length}
          itemSize={48}
          width={width}
          overscanCount={5}
        >
          {Row}
        </FixedSizeList>
      )}
    </AutoSizer>
  );
}

Eliminating Unnecessary Re-renders

Profiler shows component renders too often. Causes and solutions:

New objects/functions on every render:

// Problem: new function on every parent render
function Parent() {
  const handleClick = (id: number) => doSomething(id);  // new object
  return <Child onClick={handleClick} />;
}

// Solution: useCallback
function Parent() {
  const handleClick = useCallback((id: number) => doSomething(id), []);
  return <Child onClick={handleClick} />;
}

memo for stable components:

const ProductCard = memo(function ProductCard({ product, onAddToCart }: Props) {
  return (
    <div className="card">
      <h3>{product.name}</h3>
      <button onClick={() => onAddToCart(product.id)}>Add to cart</button>
    </div>
  );
}, (prev, next) => {
  // Custom comparator — render only if needed fields changed
  return prev.product.id === next.product.id
    && prev.product.price === next.product.price
    && prev.onAddToCart === next.onAddToCart;
});

useMemo for expensive computations:

function ProductList({ products, filters }: Props) {
  // Without useMemo — filtering on every re-render
  const filtered = useMemo(
    () => products.filter(applyFilters(filters)).sort(sortBy('price')),
    [products, filters]
  );

  return filtered.map((p) => <ProductCard key={p.id} product={p} />);
}

Context: Isolating Updates

Context triggers re-render of all consumers on any value change. Solution — split contexts by update frequency:

// Bad: everything in one context
const AppContext = createContext({ user, cart, theme, settings });

// Good: split by update frequency
const UserContext = createContext(user);          // rarely
const CartContext = createContext(cart);          // often (per item)
const ThemeContext = createContext(theme);         // very rarely

For high-frequency updates (price, ticker) — use-context-selector:

import { createContext, useContextSelector } from 'use-context-selector';

const StoreContext = createContext({ price: 0, volume: 0 });

// Re-renders only on price change, ignores volume
function PriceDisplay() {
  const price = useContextSelector(StoreContext, (s) => s.price);
  return <span>{price}</span>;
}

Deferred Rendering: React 18 Transitions

Heavy updates (filtering large list) shouldn't block input:

import { useTransition, useDeferredValue } from 'react';

function SearchWithHeavyList({ items }: { items: Item[] }) {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  // Updating query — urgent (show typed character)
  // Updating filtered list — non-urgent
  const deferredQuery = useDeferredValue(query);

  const filtered = useMemo(
    () => items.filter((item) => item.name.includes(deferredQuery)),
    [items, deferredQuery]
  );

  return (
    <>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {isPending && <span className="loading-indicator" />}
      <VirtualList items={filtered} itemHeight={48} renderItem={...} />
    </>
  );
}

Web Workers for Heavy Computations

Sorting/filtering a million records shouldn't block main thread:

// filter.worker.ts
self.onmessage = ({ data: { items, filters } }) => {
  const result = items.filter(applyFilters(filters));
  self.postMessage(result);
};
import { useEffect, useRef, useState } from 'react';

function useWorkerFilter(items: Item[], filters: Filters) {
  const [filtered, setFiltered] = useState(items);
  const workerRef = useRef<Worker>();

  useEffect(() => {
    workerRef.current = new Worker(
      new URL('./filter.worker.ts', import.meta.url),
      { type: 'module' }
    );

    workerRef.current.onmessage = ({ data }) => setFiltered(data);
    return () => workerRef.current?.terminate();
  }, []);

  useEffect(() => {
    workerRef.current?.postMessage({ items, filters });
  }, [items, filters]);

  return filtered;
}

Layout Thrashing

Alternating reads and writes of DOM properties causes forced reflow:

// Bad: read → write → read → write = 4 reflows
const h1 = el1.offsetHeight;
el2.style.height = h1 + 'px';
const h2 = el3.offsetHeight;
el4.style.height = h2 + 'px';

// Good: all reads first, then all writes
const h1 = el1.offsetHeight;
const h2 = el3.offsetHeight;
el2.style.height = h1 + 'px';
el4.style.height = h2 + 'px';

// Or via requestAnimationFrame
requestAnimationFrame(() => {
  const heights = elements.map(el => el.offsetHeight);
  requestAnimationFrame(() => {
    targets.forEach((el, i) => el.style.height = heights[i] + 'px');
  });
});

Optimization Checklist

Before optimization measure with Profiler. Then by priority:

  1. Virtualize lists > 200 elements — largest impact
  2. memo + useCallback on components with expensive renders
  3. Split contexts, isolate frequent updates
  4. useDeferredValue / startTransition for non-urgent updates
  5. useMemo for expensive computations (> 1–2 ms)
  6. Web Workers for CPU-intensive tasks

Timeline

  • Performance audit (profiling, report with recommendations): 1 day
  • Virtualize one heavy list/table: 1–2 days
  • Comprehensive optimization (virtualization + eliminate re-renders + workers): 3–7 days
  • Full performance refactor of large SPA: 2–4 weeks