Разделение кода

Разделение кода в проектах с использованием библиотеки Tom Sel ect необходимо для уменьшения размера начального бандла, ускорения загрузки интерфейса и изоляции функциональности. Особенно актуально это при использовании большого количества плагинов, асинхронных загрузчиков, пользовательских рендеров и интеграции с фреймворками.

Основная проблема монолитной сборки заключается в том, что даже неиспользуемые компоненты Tom Sel ect могут попадать в итоговый bundle. Это увеличивает время загрузки страницы и расход памяти браузера.

Разделение кода позволяет:

  • загружать Tom Select только при необходимости;
  • подключать плагины динамически;
  • отделять административные интерфейсы от публичных;
  • исключать тяжёлые зависимости из стартовой загрузки;
  • оптимизировать SSR и SPA-приложения;
  • улучшать показатели Lighthouse и Core Web Vitals.

Базовая динамическая загрузка

Самая простая форма code splitting — динамический импорт.

async function initSelect() {
    const TomSelectModule = await import('tom-select');

    const TomSelect = TomSelectModule.default;

    new TomSelect('#tags', {
        create: true
    });
}

document
    .querySelector('#open-form')
    .addEventListener('click', initSelect);

В этом случае библиотека не попадает в основной bundle и загружается только после вызова initSelect.


Разделение CSS и JavaScript

Tom Select включает как JavaScript, так и стили. Часто требуется загружать их независимо.

async function loadTomSelect() {
    await import('tom-select/dist/css/tom-select.css');

    const { default: TomSelect } = await import('tom-select');

    return TomSelect;
}

Такой подход особенно полезен при:

  • ленивой загрузке модальных окон;
  • подключении административных компонентов;
  • разделении маршрутов SPA;
  • условной инициализации UI.

Lazy initialization

Отложенная инициализация предотвращает загрузку библиотеки до момента взаимодействия пользователя с интерфейсом.

Инициализация по focus

const element = document.querySelector('#countries');

let initialized = false;

element.addEventListener('focus', async () => {
    if (initialized) return;

    initialized = true;

    const { default: TomSelect } = await import('tom-select');

    new TomSelect(element, {
        maxItems: 5
    });
});

Подобная стратегия уменьшает нагрузку при первом рендеринге страницы.


Разделение по маршрутам SPA

Во фреймворках с роутингом Tom Select обычно требуется только на отдельных страницах.

Пример для роутера

const routes = {
    '/users': async () => {
        const page = await import('./pages/users.js');

        return page.render();
    },

    '/settings': async () => {
        const page = await import('./pages/settings.js');

        return page.render();
    }
};

Внутри страницы:

export async function render() {
    const { default: TomSelect } = await import('tom-select');

    new TomSelect('#roles', {
        plugins: ['remove_button']
    });
}

Теперь Tom Select будет загружаться только на странице /settings.


Разделение плагинов

Плагины могут существенно увеличивать bundle. Особенно это касается пользовательских расширений.

Динамическое подключение плагина

async function createSelect() {
    const [
        TomSelectModule,
        pluginModule
    ] = await Promise.all([
        import('tom-select'),
        import('./plugins/custom-plugin.js')
    ]);

    const TomSelect = TomSelectModule.default;

    TomSelect.define(
        'custom_plugin',
        pluginModule.default
    );

    new TomSelect('#items', {
        plugins: ['custom_plugin']
    });
}

Асинхронная регистрация плагинов

При большом количестве расширений используется отдельный загрузчик.

const pluginLoaders = {
    tags: () => import('./plugins/tags.js'),
    users: () => import('./plugins/users.js'),
    analytics: () => import('./plugins/analytics.js')
};

async function loadPlugin(name, TomSelect) {
    const module = await pluginLoaders[name]();

    TomSelect.define(name, module.default);
}

Использование:

const { default: TomSelect } = await import('tom-select');

await loadPlugin('analytics', TomSelect);

new TomSelect('#events', {
    plugins: ['analytics']
});

Разделение пользовательских render-функций

Render-функции нередко содержат сложную HTML-логику, шаблонизаторы или форматтеры.

Вынос render в отдельный модуль

// renderers/userRenderer.js

export function renderUser(data, escape) {
    return `
        <div class="user">
            <img src="${escape(data.avatar)}">
            <span>${escape(data.name)}</span>
        </div>
    `;
}

Подключение:

const [
    TomSelectModule,
    rendererModule
] = await Promise.all([
    import('tom-select'),
    import('./renderers/userRenderer.js')
]);

const TomSelect = TomSelectModule.default;

new TomSelect('#users', {
    render: {
        option: rendererModule.renderUser,
        item: rendererModule.renderUser
    }
});

Изоляция API-логики

Асинхронная загрузка данных не должна находиться внутри конфигурации Tom Select.

Плохая архитектура:

new TomSelect('#users', {
    load(query, callback) {
        fetch(`/api/users?q=${query}`)
            .then(r => r.json())
            .then(callback);
    }
});

Лучший вариант:

// services/userService.js

export async function searchUsers(query) {
    const response = await fetch(
        `/api/users?q=${encodeURIComponent(query)}`
    );

    return response.json();
}
const [
    TomSelectModule,
    userService
] = await Promise.all([
    import('tom-select'),
    import('./services/userService.js')
]);

const TomSelect = TomSelectModule.default;

new TomSelect('#users', {
    async load(query, callback) {
        try {
            const data = await userService.searchUsers(query);

            callback(data);
        } catch {
            callback();
        }
    }
});

Использование preload-компонентов

Иногда необходимо заранее загрузить Tom Select перед открытием интерфейса.

Предварительная загрузка

let tomSelectPromise = null;

function preloadTomSelect() {
    if (!tomSelectPromise) {
        tomSelectPromise = import('tom-select');
    }

    return tomSelectPromise;
}

Использование:

document
    .querySelector('#open-modal')
    .addEventListener('mouseenter', preloadTomSelect);

Браузер начинает загрузку заранее, что уменьшает задержку при открытии модального окна.


Разделение конфигураций

Крупные конфигурации удобнее хранить отдельно.

Конфигурационный модуль

// config/userSelectConfig.js

export default {
    valueField: 'id',

    labelField: 'name',

    searchField: ['name', 'email'],

    maxOptions: 100
};

Подключение:

const [
    TomSelectModule,
    configModule
] = await Promise.all([
    import('tom-select'),
    import('./config/userSelectConfig.js')
]);

const TomSelect = TomSelectModule.default;

new TomSelect('#users', configModule.default);

Разделение по бизнес-логике

Один из лучших подходов — отделение логики формы от логики Tom Select.

Плохой пример

new TomSelect('#products', {
    onChange(value) {
        updateCart(value);
        validateOrder();
        saveState();
        sendAnalytics(value);
    }
});

Хороший пример

function handleProductChange(value) {
    updateCart(value);

    validateOrder();

    saveState();

    sendAnalytics(value);
}
new TomSelect('#products', {
    onChange: handleProductChange
});

Разделение для SSR

Tom Select зависит от DOM, поэтому прямой импорт на сервере вызывает ошибки.

Проверка окружения

if (typeof window !== 'undefined') {
    const { default: TomSelect } = await import('tom-select');

    new TomSelect('#categories');
}

Разделение в React

В React обычно используется lazy или динамический импорт внутри useEffect.

Динамическая загрузка

import { useEffect, useRef } fr om 'react';

export function UserSelect() {
    const ref = useRef(null);

    useEffect(() => {
        let instance = null;

        async function init() {
            const { default: TomSelect } = await import('tom-select');

            instance = new TomSelect(ref.current, {
                create: true
            });
        }

        init();

        return () => {
            instance?.destroy();
        };
    }, []);

    return <sel ect ref={ref}></select>;
}

Разделение в Vue

Асинхронная инициализация

import { onMounted, ref } fr om 'vue';

export default {
    setup() {
        const sel ect = ref(null);

        onMounted(async () => {
            const { default: TomSelect } =
                await import('tom-select');

            new TomSelect(select.value);
        });

        return {
            select
        };
    }
};

Разделение в Svelte

import { onMount } fr om 'svelte';

let element;

onMount(async () => {
    const { default: TomSelect } =
        await import('tom-select');

    const instance = new TomSelect(element);

    return () => {
        instance.destroy();
    };
});

Вынесение фабрик компонентов

При множестве селектов удобно создавать фабрики.

Фабрика

// factories/createTagSelect.js

export async function createTagSelect(selector) {
    const { default: TomSelect } =
        await import('tom-select');

    return new TomSelect(selector, {
        create: true,
        persist: false
    });
}

Использование:

import { createTagSelect }
    fr om './factories/createTagSelect.js';

await createTagSelect('#tags');

Выделение общих чанков

Bundler может автоматически выделять общий chunk.

Пример splitChunks в Webpack

optimization: {
    splitChunks: {
        cacheGroups: {
            tomselect: {
                test: /[\\/]node_modules[\\/]tom-select/,
                name: 'tom-select',
                chunks: 'all'
            }
        }
    }
}

Разделение тем оформления

Tom Select поддерживает несколько CSS-тем. Нет необходимости загружать все сразу.

Динамическая тема

async function loadTheme(name) {
    await import(
        `tom-select/dist/css/${name}.css`
    );
}

Асинхронные формы

Часто форма появляется только после открытия модального окна.

Lazy form loading

async function openUserModal() {
    const [
        modalModule,
        TomSelectModule
    ] = await Promise.all([
        import('./modal.js'),
        import('tom-select')
    ]);

    modalModule.open();

    const TomSelect = TomSelectModule.default;

    new TomSelect('#user-select');
}

Оптимизация памяти

Разделение кода помогает уменьшать потребление памяти при уничтожении интерфейсов.

Правильное уничтожение

const instance = new TomSelect('#tags');

instance.destroy();

При SPA-навигации это особенно важно, поскольку динамически загруженные компоненты могут оставаться в памяти.


Локальное разделение зависимостей

Некоторые страницы могут использовать собственные наборы расширений.

Пример структуры

src/
├── admin/
│   ├── adminSelect.js
│   └── plugins/
├── public/
│   ├── searchSelect.js
│   └── plugins/

Такой подход предотвращает попадание административных плагинов в публичную сборку.


Микрофронтенд-подход

В крупных системах Tom Select может существовать как изолированный модуль.

Изолированный bootstrap

export async function mount(element) {
    const { default: TomSelect } =
        await import('tom-select');

    return new TomSelect(element, {
        create: true
    });
}

Разделение загрузчиков данных

При работе с разными API загрузчики удобно изолировать.

const loaders = {
    users: () => import('./loaders/users.js'),
    tags: () => import('./loaders/tags.js'),
    products: () => import('./loaders/products.js')
};

Условная загрузка Tom Select

Иногда библиотека вообще не требуется на мобильных устройствах.

if (window.innerWidth > 768) {
    const { default: TomSelect } =
        await import('tom-select');

    new TomSelect('#desktop-select');
}

Анализ чанков

После разделения кода необходимо анализировать результат сборки.

Типичные проблемы:

  • дублирование Tom Select в нескольких чанках;
  • повторная регистрация плагинов;
  • загрузка CSS в каждом chunk;
  • неиспользуемые зависимости;
  • слишком маленькие чанки.

Для анализа обычно применяются:

  • bundle analyzer;
  • vite-bundle-visualizer;
  • source-map-explorer;
  • webpack-bundle-analyzer.

Рекомендуемая структура проекта

src/
├── components/
│   ├── selects/
│   │   ├── userSelect.js
│   │   ├── tagSelect.js
│   │   └── productSelect.js
│   │
│   ├── plugins/
│   │   ├── analytics.js
│   │   └── virtualScroll.js
│   │
│   ├── renderers/
│   │   ├── userRenderer.js
│   │   └── productRenderer.js
│   │
│   ├── services/
│   │   ├── users.js
│   │   └── products.js
│   │
│   └── config/
│       ├── users.js
│       └── products.js
│
├── pages/
├── routes/
└── app.js

Преимущества грамотного разделения

Грамотное разделение кода в проектах с Tom Select обеспечивает:

  • уменьшение стартового bundle;
  • ускорение первого рендера;
  • лучшую масштабируемость;
  • упрощение поддержки;
  • изоляцию бизнес-логики;
  • независимую загрузку плагинов;
  • снижение потребления памяти;
  • более эффективное кеширование;
  • улучшение производительности SPA и SSR-приложений;
  • удобную модульную архитектуру.