Setting up asynchronous JS loading for 1C-Bitrix

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1175
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

Async JS Loading Configuration for 1C-Bitrix

The most common symptom: Lighthouse reports "Eliminate render-blocking resources", and the list includes jQuery, Swiper, and Bitrix component scripts. The browser parses HTML, encounters a <script src="..."> without attributes, and stops rendering until the file is downloaded and executed. A typical Bitrix site accumulates 5–10 such blocking scripts.

How Bitrix Manages JS

Bitrix registers scripts via CMain::AddHeadScript() and Asset::getInstance()->addJs(). The ShowHead() method outputs them in <head> without defer/async. Components add scripts via $APPLICATION->AddHeadScript() — the same behavior. The only way to override this is at the template level or by post-processing the output buffer.

Adding defer/async via the Template

The safest approach is to use the OnEndBufferContent event for post-processing the final HTML:

// local/php_interface/init.php
AddEventHandler('main', 'OnEndBufferContent', 'deferScripts');

function deferScripts(string &$content): void
{
    // Add defer to all external scripts except explicitly excluded ones
    $exclude = ['jquery.min.js', '/bitrix/js/main/core/core.js'];

    $content = preg_replace_callback(
        '/<script\s([^>]*src=["\'][^"\']+["\'][^>]*)>/i',
        function (array $m) use ($exclude): string {
            foreach ($exclude as $ex) {
                if (str_contains($m[0], $ex)) {
                    return $m[0];
                }
            }
            if (str_contains($m[1], 'defer') || str_contains($m[1], 'async')) {
                return $m[0];
            }
            return '<script ' . $m[1] . ' defer>';
        },
        $content
    );
}

jQuery and Bitrix's core.js are excluded from defer — component initialization depends on them. Everything else receives defer.

Asset Manager and Dependency Groups

Since Bitrix 14+, \Bitrix\Main\Page\Asset is available. Scripts can be registered with a position specifier:

\Bitrix\Main\Page\Asset::getInstance()->addJs(
    '/local/js/mymodule.js',
    false,  // do not bundle
    \Bitrix\Main\Page\Asset::POS_AFTER  // after </body>
);

POS_AFTER places the script before </body> — effectively equivalent to defer for independent modules. For scripts that need DOM-ready, this is preferable to async.

When defer Cannot Be Used

Scripts that cannot be deferred without consequences:

  • jQuery, if other scripts in the page body call $() inline
  • Bitrix's core.js and ajax.js — the BX core
  • Analytics counters, if they measure time to interactivity
  • A/B testing scripts (they modify the DOM before rendering)

For these, use <link rel="preload" as="script"> — the browser downloads the file with high priority, but execution happens in the order controlled by the developer.

Case Study: Travel Company Website

A site on Bitrix "Start" with a search form on the home page. TBT (Total Blocking Time) in Lighthouse — 1,840 ms. Cause: 12 scripts in <head>, including Swiper 8.1 (120 KB), Fancybox (80 KB), and Yandex Maps (async API, but blocking initialization).

After adding defer via OnEndBufferContent and migrating the map to deferred initialization via IntersectionObserver:

  • TBT: 1,840 ms → 240 ms
  • TTI (Time to Interactive): 6.1 s → 2.8 s
  • Lighthouse Performance score: 34 → 78

Work: 1 day.