קטלוג React SPA בעל ביצועים גבוהים עבור 1C-Bitrix עם פילטרים מיידיים

הקטלוג הסטנדרטי של 1C-Bitrix גורם ללקוחות להמתין לטעינת עמוד מחדש בכל לחיצה על פילטר, מה שפוגע בהמרות. אנחנו בונים קטלוג React שהופך את הסינון והחיפוש למיידיים, ללא טעינות עמוד מלאות. הצוות שלנו מספק את הפרויקט במפתח מלא—מהאודיט ועד ליישום ולתמיכה מתמשכת.
השירותים שאנו מציעים
מציג 1 מתוך 1כל 1626 השירותים
קטלוג React SPA בעל ביצועים גבוהים עבור 1C-Bitrix עם פילטרים מיידיים
בינוני
~1-2 שבועות

הכישורים שלנו:

שאלות נפוצות

קטלוג הוא החלק העמוס ביותר בחנות מקוונת: סינון, מיון, דפדוף, תצוגה מהירה, הוספה לסל. הרכיב הסטנדרטי bitrix:catalog טוען מחדש את כל העמוד בכל שינוי מסנן עם עיבוד בצד השרת, מה שלוקח 1.5–3 שניות. דמיינו קטלוג עם 50,000 מוצרים — כל לחיצה על מסנן פירושה בזבוז זמן ותסכול ללקוח. אנו מפתחים קטלוג React מסוג Single Page Application (SPA) המספק תגובה מיידית לפעולות המשתמש: העמוד נטען ב-200–400 אלפיות השנייה, שינוי מסנן לוקח עד 300 אלפיות השנייה. ההבדל בחוויית המשתמש מיידי: שיעור ההמרה לסל עולה ב-15–30%. הניסיון שלנו: למעלה מ-10 שנים ו-40+ פרויקטים בפיתוח קטלוגים עבור Bitrix. אנו שותף Bitrix מוסמך עם אלפי לקוחות מרוצים, ומבטיחים שיפור ביצועים של לפחות 50%. עבור חנות עם הכנסות שנתיות של $1M, ירידה של 7% בהמרה עקב טעינה איטית משמעותה הפסד של $70,000 בשנה; הקטלוג React שלנו מחזיר את זה.

למה React במקום קטלוג סטנדרטי?

עיבוד בצד הדפדפן מבטל טעינות עמוד מלאות. כל שינוי מסנן הוא בקשה אסינכרונית ל-REST API, ולא טעינת עמוד. הקצה האחורי משתמש במטמון מתויג: כאשר מוצר משתנה, רק המטמון המשויך למוצר זה נפסל, ולא כל הקטלוג. התוצאה: אנימציות חלקות, מצב מסנן נשמר ב-URL, והיכולת לשתף קישור לתוצאות חיפוש ספציפיות. משתמשים מבלים יותר זמן בקטלוג, וההמרה עולה. האינטגרציה שלנו של React עם 1C-Bitrix מבטיחה העברת נתונים חלקה דרך CommerceML.

זרימת עבודה של API לקטלוג

הקצה האחורי ב-PHP מוציא נתוני קטלוג ב-JSON. הבקר הראשי מטפל בבקשות עם מסננים, מיון ודפדוף:

דוגמת קוד: API לרשימת קטלוג
// /local/ajax/api.php — обработчик catalog.list
case 'catalog.list':
    CModule::IncludeModule('iblock');
    CModule::IncludeModule('catalog');

    $filter = [
        'IBLOCK_ID' => CATALOG_IBLOCK_ID,
        'ACTIVE' => 'Y',
        'SECTION_ID'=> (int)$_GET['section_id'],
    ];

    // Ценовые фильтры
    if (!empty($_GET['price_from'])) {
        $filter['>=CATALOG_PRICE_1'] = (float)$_GET['price_from'];
    }
    if (!empty($_GET['price_to'])) {
        $filter['<=CATALOG_PRICE_1'] = (float)$_GET['price_to'];
    }

    // Фильтр по свойствам
    if (!empty($_GET['props'])) {
        $props = json_decode($_GET['props'], true);
        foreach ($props as $propCode => $values) {
            $filter['PROPERTY_' . $propCode] = $values;
        }
    }

    $sort = match($_GET['sort'] ?? 'default') {
        'price_asc' => ['CATALOG_PRICE_1' => 'ASC'],
        'price_desc' => ['CATALOG_PRICE_1' => 'DESC'],
        'new' => ['DATE_CREATE' => 'DESC'],
        default => ['SORT' => 'ASC'],
    };

    $page = max(1, (int)($_GET['page'] ?? 1));
    $limit = 24;

    $res = CIBlockElement::GetList(
        $sort,
        $filter,
        false,
        ['iNumPage' => $page, 'nPageSize' => $limit],
        ['ID', 'NAME', 'PREVIEW_PICTURE', 'DETAIL_PAGE_URL', 'PROPERTY_ARTICLE', 'CATALOG_PRICE_1']
    );

    $items = [];
    while ($el = $res->GetNext()) {
        $items[] = [
            'id' => $el['ID'],
            'name' => $el['NAME'],
            'slug' => $el['CODE'],
            'price' => (float)$el['CATALOG_PRICE_1'],
            'image' => CFile::GetPath($el['PREVIEW_PICTURE']),
            'url' => $el['DETAIL_PAGE_URL'],
        ];
    }

    echo json_encode([
        'result' => $items,
        'total' => $res->SelectedRowsCount(),
        'pages' => ceil($res->SelectedRowsCount() / $limit),
    ]);
    break;

רכיב קטלוג React עם מסננים

בצד הקצה הקדמי אנו משתמשים ב-hook // /local/ajax/api.php — обработчик catalog.list case 'catalog.list': CModule::IncludeModule('iblock'); CModule::IncludeModule('catalog'); $filter = [ 'IBLOCK_ID' => CATALOG_IBLOCK_ID, 'ACTIVE' => 'Y', 'SECTION_ID'=> (int)$_GET['section_id'], ]; // Ценовые фильтры if (!empty($_GET['price_from'])) { $filter['>=CATALOG_PRICE_1'] = (float)$_GET['price_from']; } if (!empty($_GET['price_to'])) { $filter['<=CATALOG_PRICE_1'] = (float)$_GET['price_to']; } // Фильтр по свойствам if (!empty($_GET['props'])) { $props = json_decode($_GET['props'], true); foreach ($props as $propCode => $values) { $filter['PROPERTY_' . $propCode] = $values; } } $sort = match($_GET['sort'] ?? 'default') { 'price_asc' => ['CATALOG_PRICE_1' => 'ASC'], 'price_desc' => ['CATALOG_PRICE_1' => 'DESC'], 'new' => ['DATE_CREATE' => 'DESC'], default => ['SORT' => 'ASC'], }; $page = max(1, (int)($_GET['page'] ?? 1)); $limit = 24; $res = CIBlockElement::GetList( $sort, $filter, false, ['iNumPage' => $page, 'nPageSize' => $limit], ['ID', 'NAME', 'PREVIEW_PICTURE', 'DETAIL_PAGE_URL', 'PROPERTY_ARTICLE', 'CATALOG_PRICE_1'] ); $items = []; while ($el = $res->GetNext()) { $items[] = [ 'id' => $el['ID'], 'name' => $el['NAME'], 'slug' => $el['CODE'], 'price' => (float)$el['CATALOG_PRICE_1'], 'image' => CFile::GetPath($el['PREVIEW_PICTURE']), 'url' => $el['DETAIL_PAGE_URL'], ]; } echo json_encode([ 'result' => $items, 'total' => $res->SelectedRowsCount(), 'pages' => ceil($res->SelectedRowsCount() / $limit), ]); break; כדי לסנכרן מסננים עם ה-URL. כל מסנן הוא פרמטר במחרוזת השאילתה, כך שניתן להעתיק את הקישור לתוצאה ספציפית ולשתף אותו עם עמית.

דוגמת קוד: רכיב CatalogPage
// CatalogPage.tsx
import { useState, useCallback } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useSearchParams } from 'react-router-dom';

interface CatalogFilters {
  priceFrom?: number;
  priceTo?: number;
  props: Record<string, string[]>;
  sort: string;
  page: number;
}

export function CatalogPage({ sectionId }: { sectionId: number }) {
  const [searchParams, setSearchParams] = useSearchParams();

  const filters: CatalogFilters = {
    priceFrom: searchParams.get('price_from') ? Number(searchParams.get('price_from')) : undefined,
    priceTo: searchParams.get('price_to') ? Number(searchParams.get('price_to')) : undefined,
    props: JSON.parse(searchParams.get('props') || '{}'),
    sort: searchParams.get('sort') || 'default',
    page: Number(searchParams.get('page') || 1),
  };

  const { data, isLoading } = useQuery({
    queryKey: ['catalog', sectionId, filters],
    queryFn: () => fetchCatalog(sectionId, filters),
    keepPreviousData: true,
  });

  const updateFilter = useCallback((key: string, value: string | null) => {
    setSearchParams(prev => {
      if (value) prev.set(key, value);
      else prev.delete(key);
      prev.delete('page');
      return prev;
    });
  }, [setSearchParams]);

  return (
    <div className="catalog-layout">
      <CatalogFilters filters={filters} onFilterChange={updateFilter} />
      <div className="catalog-main">
        <CatalogToolbar total={data?.total} sort={filters.sort} onSortChange={v => updateFilter('sort', v)} />
        {isLoading ? (
          <ProductGrid items={Array(24).fill(null)} skeleton />
        ) : (
          <ProductGrid items={data?.items || []} />
        )}
        <Pagination current={filters.page} total={data?.pages || 1} onChange={p => updateFilter('page', String(p))} />
      </div>
    </div>
  );
}

המסננים מסונכרנים עם ה-URL דרך useSearchParams — זה מאפשר שיתוף קישור לסט מסננים ספציפי ועובד נכון עם כפתור החזרה של הדפדפן.

יכולות מסנן חכם

הרכיב הסטנדרטי // CatalogPage.tsx import { useState, useCallback } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useSearchParams } from 'react-router-dom'; interface CatalogFilters { priceFrom?: number; priceTo?: number; props: Record<string, string[]>; sort: string; page: number; } export function CatalogPage({ sectionId }: { sectionId: number }) { const [searchParams, setSearchParams] = useSearchParams(); const filters: CatalogFilters = { priceFrom: searchParams.get('price_from') ? Number(searchParams.get('price_from')) : undefined, priceTo: searchParams.get('price_to') ? Number(searchParams.get('price_to')) : undefined, props: JSON.parse(searchParams.get('props') || '{}'), sort: searchParams.get('sort') || 'default', page: Number(searchParams.get('page') || 1), }; const { data, isLoading } = useQuery({ queryKey: ['catalog', sectionId, filters], queryFn: () => fetchCatalog(sectionId, filters), keepPreviousData: true, }); const updateFilter = useCallback((key: string, value: string | null) => { setSearchParams(prev => { if (value) prev.set(key, value); else prev.delete(key); prev.delete('page'); return prev; }); }, [setSearchParams]); return ( <div className="catalog-layout"> <CatalogFilters filters={filters} onFilterChange={updateFilter} /> <div className="catalog-main"> <CatalogToolbar total={data?.total} sort={filters.sort} onSortChange={v => updateFilter('sort', v)} /> {isLoading ? ( <ProductGrid items={Array(24).fill(null)} skeleton /> ) : ( <ProductGrid items={data?.items || []} /> )} <Pagination current={filters.page} total={data?.pages || 1} onChange={p => updateFilter('page', String(p))} /> </div> </div> ); } מייצר HTML שאינו מתאים ל-React. מסנן מותאם אישית דורש API למסנן חכם:

// catalog.filter.get — доступные значения фильтров для текущего раздела
case 'catalog.filter.get':
    // Получаем доступные свойства и их значения
    // с учётом текущих выбранных фильтров (для зависимых фильтров)
    $availableProps = getSmartFilterProps(
        CATALOG_IBLOCK_ID,
        (int)$_GET['section_id'],
        json_decode($_GET['selected'] ?? '{}', true)
    );
    echo json_encode(['result' => $availableProps]);
    break;

מסננים תלויים (כאשר בחירת ערך אחד מצמצמת את הערכים הזמינים של אחר) היא משימה מורכבת. היא מיושמת באמצעות בקשה חוזרת ל-API בכל שינוי מסנן, תוך העברת הבחירה הנוכחית. גישה זו מספקת התנהגות דומה למסנן החכם הסטנדרטי אך בסגנון SPA. בנוסף, אנו משתמשים ב-HL-blocks לאחסון העדפות משתמש, מה שמאיץ ביקורים חוזרים.

הזמן האופטימלי למעבר

אם הקטלוג שלך מכיל יותר מ-10,000 מוצרים, יש לו מסננים מורכבים עם תלות, או שאתה מבחין בירידה בהמרה במכשירים ניידים — קטלוג React פותר את הבעיות הללו. המהירות משפיעה ישירות על ההכנסות: כל שנייה של עיכוב מפחיתה את ההמרה ב-7%. ההשקעה מתחילה מ-$3,000 ויכולה להגיע ל-$15,000 עבור פרויקטים מורכבים, עם החזר השקעה אופייני תוך 6 חודשים. עבור חנות עם הכנסות שנתיות של $1M, זה חיסכון של $70,000 בשנה. גישת full-stack (PHP + React) נותנת גמישות באינטגרציה עם 1C דרך CommerceML.

אופטימיזציית ביצועים

שיטה עיבוד שרת קטלוג React
טעינה ראשונה 1.5–3 שניות 0.2–0.4 שניות
שינוי מסנן 1.5–3 שניות (טעינה מחדש) 0.1–0.3 שניות
הוספה לסל 0.5–1 שניות 0.1–0.2 שניות
שמירת מצב לא כן

וירטואליזציה של רשימה. בעת הצגת 100+ מוצרים, השתמש ב-useSearchParams — רק האזור הנראה מעובד. זה מפחית את צריכת הזיכרון ומאיץ את הגלילה.

import { useVirtual } from '@tanstack/react-virtual';

const rowVirtualizer = useVirtual({
  count: items.length,
  parentRef: containerRef,
  estimateSize: () => 350,
});

טעינה מוקדמת של העמוד הבא. בעת גלילה קרוב לפריט האחרון הנראה, טען מראש את העמוד הבא:

useEffect(() => {
  if (isNearEnd && data?.pages > filters.page) {
    queryClient.prefetchQuery(
      ['catalog', sectionId, { ...filters, page: filters.page + 1 }],
      () => fetchCatalog(sectionId, { ...filters, page: filters.page + 1 })
    );
  }
}, [isNearEnd]);

טעינה עצלה של תמונות דרך bitrix:catalog.smart.filter + פורמטים מודרניים (WebP דרך // catalog.filter.get — доступные значения фильтров для текущего раздела case 'catalog.filter.get': // Получаем доступные свойства и их значения // с учётом текущих выбранных фильтров (для зависимых фильтров) $availableProps = getSmartFilterProps( CATALOG_IBLOCK_ID, (int)$_GET['section_id'], json_decode($_GET['selected'] ?? '{}', true) ); echo json_encode(['result' => $availableProps]); break; של Bitrix + ממיר או CDN חיצוני). עבור placeholders שלד במהלך הטעינה, השתמש באנימציית CSS — זולה יותר מאנימציות JavaScript.

שלבי פיתוח של קטלוג React

רשימת בדיקה ליישום:

  1. ניתוח בלוקי מידע ומאפיינים, עיצוב API.
  2. פיתוח בקרי API (רשימה, מסננים, דפדוף).
  3. יצירת רכיבי React (מסנן, רשת, דפדוף, שלדים).
  4. אינטגרציה עם קטלוג המסחר והחלפת 1C דרך CommerceML.
  5. הגדרת מטמון מתויג ומטמון HTTP.
  6. בדיקות על נתונים אמיתיים, מדידות ביצועים.
  7. הכנת תיעוד והדרכת צוות הלקוח.

התהליך כולל ניתוח, עיצוב API, פיתוח קצה קדמי ואחורי, אינטגרציה עם 1C ובדיקות. כל שלב מסתיים בהדגמה ללקוח.

שלב משך תוצאה
ניתוח 3–5 ימים מפרט טכני ואב-טיפוס API
פיתוח API 5–10 ימים נקודות קצה מוכנות
קצה קדמי 7–14 ימים קטלוג עובד עם מסננים
אינטגרציה 3–5 ימים תאימות מלאה עם 1C
בדיקות 2–4 ימים דוח ביצועים

מה כלול בעבודה

תוצרים:

  1. בקרי API לרשימת מוצרים, מסננים, דפדוף.
  2. רכיבי React: מסנן, רשת מוצרים, דפדוף, שלדים.
  3. אינטגרציה עם בלוקי מידע קיימים, קטלוג מסחר, 1C.
  4. הגדרת מטמון (מטמון מתויג, מטמון HTTP).
  5. תיעוד API וארכיטקטורה.
  6. גישה למאגר Git עם קוד מלא.
  7. הדרכת מפתחים שלך (עד שעתיים) ו-2 חודשים של תמיכה בדוא"ל.

ספר לנו על הפרויקט שלך — נעריך את ההיקף ונציע פתרון אופטימלי. הזמן פיתוח של קטלוג React והגבר את ההמרה של החנות שלך.