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.
How rendering and caching fit together
Every [tablecrafter] instance moves through three layers, each with its own performance lever:
- 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. - 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. - 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.
| Attribute | Default | Required | Effect on performance |
|---|---|---|---|
| per_page | 0 | Optional | Sets the client page size. 0 lets TableCrafter auto-tune based on dataset size. Any positive value forces pagination on with that page size. |
| auto_refresh | false | Optional | Enables the client-side polling loop. Off by default so static tables incur no background fetches. |
| refresh_interval | 300000 | Optional | Polling interval in milliseconds (default 5 minutes). Lower values mean fresher data but more upstream requests. |
| refresh_indicator | true | Optional | Shows the refresh status widget with pause and "refresh now" controls. |
| refresh_countdown | false | Optional | Adds a live countdown to the next refresh. Slightly more DOM churn. |
| refresh_last_updated | true | Optional | Displays 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) | Behavior | Page size |
|---|---|---|
| ≤ 1,000 | Standard client-side pagination | 25 (default) |
| > 1,000 | Pagination forced on; large-dataset optimizations engaged; pagination info shows an (Optimized) hint | 25 |
| > 2,000 | Same as above, tuned page size | 25 |
| > 5,000 | Virtual scrolling enabled (renders only the visible window) | 50 – 100 |
| > 10,000 | All of the above, plus a console warning recommending server-side pagination | 100 |
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:
- Transient TTL = 1 hour. Both the rendered HTML (
tc_html_*) and the raw fetched data (tc_cache_*) expire afterHOUR_IN_SECONDS. This is the hard ceiling on how long a cache entry can live untouched. - Stale threshold = 5 minutes. Within the hour, once an entry is older than 5 minutes a page view will trigger an invisible background rebuild via
tc_refresh_single_sourcewhile still serving the cached copy.
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.
| Interval | Value | Best for | Cost |
|---|---|---|---|
| 30 seconds | 30000 | Live status boards, inventory counts | High — one upstream request per tab every 30s |
| 5 minutes (default) | 300000 | Most operational dashboards | Balanced |
| 15+ minutes | 900000 | Slow-moving reference data | Low |
<!-- 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:
- Pauses while the user interacts with the table (editing, filtering, sorting) so a refresh never wipes out in-progress work.
- Pauses when the tab is hidden (via the Page Visibility API), eliminating background polling on inactive tabs.
- Retries on failure up to 3 attempts before backing off, so a single flaky upstream response does not break the cycle.
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:
| Class | Element |
|---|---|
| .tc-refresh-indicator | Wrapper for the whole widget |
| .tc-refresh-status | Status text and icon group |
| .tc-refresh-toggle | Pause / resume button |
| .tc-refresh-manual | "Refresh now" button |
| .tc-countdown | Countdown timer (only when refresh_countdown is true) |
| .tc-last-updated | Last-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
- Watch the dataset hint. When a table crosses 1,000 rows the pagination info line appends (Optimized), confirming large-dataset mode is active.
- Check the console. Datasets above 10,000 rows log a warning recommending server-side pagination at the source.
- Listen for events. The engine dispatches namespaced
CustomEvents on the container (for exampletablecrafter:cardTap), which you can hook for custom telemetry. - Confirm SSR hydration. A server-rendered container carries
data-ssr="true"until the JavaScript hydrates it; if it never flips, the initial data payload failed to parse.
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
- Leave
per_page="0"and let auto-tuning pick a page size unless you have a specific device-performance target. - Keep the default 1-hour transient cache; rely on the 5-minute SWR window for freshness on cached HTML.
- Enable
auto_refreshonly on tables that genuinely need live updates, and start at the 5-minute default before lowering it. - Schedule
wp tablecrafter warm-cacheahead of traffic peaks, and runwp tablecrafter clear-cacheafter 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.