Optimizing FID (First Input Delay) for 1C-Bitrix
FID (First Input Delay) — the time from the first user interaction with a page until the moment the browser begins processing that interaction. Since March 2024, Google has replaced FID with INP (Interaction to Next Paint), which measures all interactions, not just the first. In practice, optimization is the same for both metrics.
Threshold: FID < 100 ms, INP < 200 ms. On heavy Bitrix sites, INP can reach 500–1500 ms.
Why the browser doesn't respond to a click
The browser is single-threaded: while the main thread is busy executing JavaScript, it cannot process input events. The user clicks a button — the click is queued and waits for the JS to complete its current task.
Sources of long tasks (Long Tasks, > 50 ms) in Bitrix:
- Loading and parsing large JS bundles: jQuery + plugins + components = 500 KB — 1 MB
- Initializing sliders, mask fields, maps, widgets on
DOMContentLoaded - Heavy event handlers: catalog filter, cart recalculation
- Synchronous AJAX requests (block the thread)
Diagnosing Long Tasks
Chrome DevTools → Performance → record while loading the page. Red bars over Tasks — these are Long Tasks > 50 ms. Click on a task — DevTools will show the call stack: which script occupied the main thread.
For INP: DevTools → Performance → enable "Web Vitals" → interact with the page → find INP events.
Console monitoring:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.warn('Long task:', entry.duration.toFixed(1) + 'ms', entry);
}
}
});
observer.observe({ type: 'longtask', buffered: true });
Bundle splitting and code splitting
Standard Bitrix loads all JS on every page: jQuery, catalog plugins, cart scripts, sliders, maps — everything at once. A page with 1 MB JS executes it entirely on load.
Transition to code splitting: each component loads only the necessary scripts:
// Load slider only if there's a slider on the page
if (document.querySelector('.main-slider')) {
import('./swiper.min.js').then(({ default: Swiper }) => {
new Swiper('.main-slider', { /* ... */ });
});
}
// Map only on contacts page
if (document.querySelector('#map')) {
import('./ymaps-init.js');
}
In the Bitrix context — via \Bitrix\Main\Page\Asset::addJs() in specific component templates, not in header.php.
Defer and async for scripts
<!-- Synchronous — blocks HTML parsing -->
<script src="/bitrix/js/plugin.js"></script>
<!-- defer — loads in parallel, executes after HTML parsing -->
<script src="/bitrix/js/plugin.js" defer></script>
<!-- async — loads and executes as early as possible -->
<script src="/bitrix/js/analytics.js" async></script>
defer — for scripts that need the DOM (component initialization).
async — for independent scripts (analytics, ad tags).
In Bitrix, JS files added via \Bitrix\Main\Page\Asset::addJs() are output without defer. To add the attribute — custom implementation via OnEndBufferContent hook or template override.
Heavy event handlers
An event handler that performs a synchronous DOM recalculation on 200 elements blocks the thread for the duration of that recalculation. INP will equal the handler time.
Improvement patterns:
Debounce for frequent events (search input, filter change):
let debounceTimer;
searchInput.addEventListener('input', function() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
doSearch(this.value);
}, 300);
});
Breaking heavy operations with setTimeout / scheduler.postTask:
async function processLargeList(items) {
for (let i = 0; i < items.length; i += 50) {
const chunk = items.slice(i, i + 50);
processChunk(chunk);
// Yield control to browser between batches
await new Promise(resolve => setTimeout(resolve, 0));
}
}
Web Workers for computations: if the event handler needs heavy calculations (sorting, filtering a large data array) — move to Web Worker. Runs in a separate thread, doesn't block the UI.
Third-party scripts
JivoSite, MetrikaTag, Google Analytics, social media pixels — each adds JS executed in the main thread. With 5–10 third-party scripts, the cumulative startup load can be 200–500 ms of Long Tasks.
Strategy:
- Load third-party scripts via
asyncor afterloadevent - Use
requestIdleCallbackfor non-critical scripts - Check if all connected widgets are needed — often there are unused ones
// Load analytics after idle
window.addEventListener('load', () => {
requestIdleCallback(() => {
const script = document.createElement('script');
script.src = 'https://analytics-provider.com/tag.js';
script.async = true;
document.head.appendChild(script);
});
});
Timeline for optimization
| Task | Timeline | Effect |
|---|---|---|
| Audit Long Tasks via DevTools | 0.5 day | Problem understanding |
| Convert scripts to defer | 1 day | INP −50–200 ms |
| Defer third-party scripts | 0.5 day | INP −100–300 ms |
| Debounce search and filters | 1 day | Filter INP −200–500 ms |
| Code splitting for heavy components | 3–5 days | Catalog pages INP −200–500 ms |
| Refactor heavy event handlers | 2–5 days | INP < 200 ms |
A good result for a Bitrix store is INP < 200 ms. This is achieved with a total JS bundle < 200 KB (after parse) and no Long Tasks > 100 ms on interaction.







