Performance Tuning

TableCrafter renders remote JSON into interactive tables entirely in the browser, backed by a server-side Stale-While-Revalidate (SWR) cache. This guide explains how pagination, virtual scrolling, cache lifetime, and the auto-refresh interval interact so you can keep large tables fast without serving stale data.

SWR Cache Pagination Virtual Scrolling Auto-Refresh Cache Warming

How rendering and caching fit together

Every [tablecrafter] instance moves through three layers, each with its own performance lever:

  1. Server-side first paint (PHP). On the first request the render_table() handler fetches the source, builds a crawlable HTML table, and stores it in a transient for one hour. Subsequent visitors get that cached HTML instantly.
  2. SWR background refresh (WP-Cron). If the cached HTML is older than five minutes, the page still serves the cached copy and schedules a single background event (tc_refresh_single_source) to rebuild it. Visitors never wait on a slow upstream API.
  3. Client hydration (JS). The embedded data payload (a <script class="tc-initial-data"> block) hydrates the JavaScript table, which then owns sorting, filtering, pagination, and virtual scrolling.
â„šī¸

The HTML cache key is derived from source, include, exclude, search, filters, export, per_page, sort, and the plugin version. Changing any of those attributes produces a fresh cache entry rather than colliding with an existing one.

Performance-related shortcode attributes

These are the attributes that directly affect rendering cost and freshness. All other attributes (such as search or export) are documented on the shortcode reference page.

AttributeDefaultRequiredEffect on performance
per_page0OptionalSets the client page size. 0 lets TableCrafter auto-tune based on dataset size. Any positive value forces pagination on with that page size.
auto_refreshfalseOptionalEnables the client-side polling loop. Off by default so static tables incur no background fetches.
refresh_interval300000OptionalPolling interval in milliseconds (default 5 minutes). Lower values mean fresher data but more upstream requests.
refresh_indicatortrueOptionalShows the refresh status widget with pause and "refresh now" controls.
refresh_countdownfalseOptionalAdds a live countdown to the next refresh. Slightly more DOM churn.
refresh_last_updatedtrueOptionalDisplays the timestamp of the last successful refresh.

Tuning for large datasets

The JavaScript engine inspects the row count after the data loads and auto-tunes its own configuration in optimizeForDatasetSize(). You normally do not need to set anything, but it helps to know the thresholds:

Dataset size (rows)BehaviorPage size
≤ 1,000Standard client-side pagination25 (default)
> 1,000Pagination forced on; large-dataset optimizations engaged; pagination info shows an (Optimized) hint25
> 2,000Same as above, tuned page size25
> 5,000Virtual scrolling enabled (renders only the visible window)50100
> 10,000All of the above, plus a console warning recommending server-side pagination100

A separate PHP-side optimizer (TC_Performance_Optimizer) engages virtual scrolling at 500 rows when its tablecrafter_render_data filter is applied, rendering an initial window of 50 rows plus a 10-row buffer and streaming the rest over the tc_virtual_scroll_data AJAX endpoint. The thresholds it exposes to JavaScript live on the tcPerformance global.

💡

For very wide or media-heavy tables, set per_page explicitly (for example per_page="50") rather than relying on auto-tuning. Smaller pages reduce the number of DOM nodes painted per page and keep scroll performance smooth on low-end devices.

Choosing a page size

When pagination is active, end users can also pick a page size from a built-in selector offering 10, 25, 50, 100, and 250. Use the shortcode per_page to set the initial value:

<!-- Force 50 rows per page for a 4,000-row catalog -->
[tablecrafter source="https://example.com/data/catalog.json" per_page=50 search="true"]

Cache lifetime and the SWR window

Two timing constants govern freshness, and they are independent:

The net effect: the first visitor after the 5-minute mark pays nothing extra, and the next visitor sees fresh data. Upstream APIs are never hit on the critical render path once a cache exists.

âš ī¸

Caches are keyed by source URL plus rendering attributes. If your data updates more often than every 5 minutes and you need server-rendered HTML to reflect it, combine the SWR layer with client-side auto_refresh rather than trying to shorten the transient TTL, which is fixed in code.

Warming and clearing the cache

TableCrafter tracks the most recently requested source URLs (up to 50) and warms them on an hourly cron job (tc_refresher_cron). You can drive both operations manually with WP-CLI:

# Rebuild caches for every tracked source URL
wp tablecrafter warm-cache

# Drop all TableCrafter transients (HTML, data, export, rate-limit)
wp tablecrafter clear-cache

Run clear-cache after changing a source schema or upgrading the plugin to avoid serving a stale structure. Schedule warm-cache just before peak traffic so the first real visitor hits a hot cache.

Auto-refresh interval trade-offs

Client-side auto-refresh is a polling loop driven by refresh_interval (milliseconds). It is the right tool for dashboards that must update without a page reload, but every active tab issues a request on each tick, so the interval is a direct cost/freshness dial.

IntervalValueBest forCost
30 seconds30000Live status boards, inventory countsHigh — one upstream request per tab every 30s
5 minutes (default)300000Most operational dashboardsBalanced
15+ minutes900000Slow-moving reference dataLow
<!-- Live dashboard: refresh every 60s, show countdown -->
[tablecrafter
  source="https://example.com/api/orders.json"
  auto_refresh="true"
  refresh_interval=60000
  refresh_countdown="true"
  per_page=25]

The refresh engine is deliberately conservative to avoid wasted requests:

💡

Auto-refresh and the SWR transient cache are complementary. The 5-minute SWR window keeps server-rendered HTML fresh for SEO and first paint; auto-refresh keeps an already-open tab live. For a fast-moving board, a 60-second refresh_interval pairs well with the default 1-hour transient because polling reads through the same cached data layer.

The refresh indicator UI

When refresh_indicator is on (the default for auto-refreshing tables), TableCrafter injects a status widget you can target or theme with CSS. The relevant classes are:

ClassElement
.tc-refresh-indicatorWrapper for the whole widget
.tc-refresh-statusStatus text and icon group
.tc-refresh-togglePause / resume button
.tc-refresh-manual"Refresh now" button
.tc-countdownCountdown timer (only when refresh_countdown is true)
.tc-last-updatedLast-updated timestamp (only when refresh_last_updated is true)

Pagination controls expose .tc-pagination, .tc-pagination-info, .tc-pagination-controls, .tc-page-jump, .tc-page-input, and .tc-page-size-select, so you can restyle the large-dataset navigation to match your theme.

Measuring and diagnosing

âš ī¸

If an upstream source returns tens of thousands of rows on every request, no amount of client tuning fully fixes the transfer cost. Paginate or filter at the API/source level and point source at the reduced endpoint, then let virtual scrolling handle whatever still arrives.

Recommended baseline

  1. Leave per_page="0" and let auto-tuning pick a page size unless you have a specific device-performance target.
  2. Keep the default 1-hour transient cache; rely on the 5-minute SWR window for freshness on cached HTML.
  3. Enable auto_refresh only on tables that genuinely need live updates, and start at the 5-minute default before lowering it.
  4. Schedule wp tablecrafter warm-cache ahead of traffic peaks, and run wp tablecrafter clear-cache after schema or version changes.

Next, see large-datasets.html for source-side pagination patterns and shortcode-reference.html for the full attribute list referenced above.