קטלוג הוא החלק העמוס ביותר בחנות מקוונת: סינון, מיון, דפדוף, תצוגה מהירה, הוספה לסל. הרכיב הסטנדרטי 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
רשימת בדיקה ליישום:
- ניתוח בלוקי מידע ומאפיינים, עיצוב API.
- פיתוח בקרי API (רשימה, מסננים, דפדוף).
- יצירת רכיבי React (מסנן, רשת, דפדוף, שלדים).
- אינטגרציה עם קטלוג המסחר והחלפת 1C דרך CommerceML.
- הגדרת מטמון מתויג ומטמון HTTP.
- בדיקות על נתונים אמיתיים, מדידות ביצועים.
- הכנת תיעוד והדרכת צוות הלקוח.
התהליך כולל ניתוח, עיצוב API, פיתוח קצה קדמי ואחורי, אינטגרציה עם 1C ובדיקות. כל שלב מסתיים בהדגמה ללקוח.
| שלב | משך | תוצאה |
|---|---|---|
| ניתוח | 3–5 ימים | מפרט טכני ואב-טיפוס API |
| פיתוח API | 5–10 ימים | נקודות קצה מוכנות |
| קצה קדמי | 7–14 ימים | קטלוג עובד עם מסננים |
| אינטגרציה | 3–5 ימים | תאימות מלאה עם 1C |
| בדיקות | 2–4 ימים | דוח ביצועים |
מה כלול בעבודה
תוצרים:
- בקרי API לרשימת מוצרים, מסננים, דפדוף.
- רכיבי React: מסנן, רשת מוצרים, דפדוף, שלדים.
- אינטגרציה עם בלוקי מידע קיימים, קטלוג מסחר, 1C.
- הגדרת מטמון (מטמון מתויג, מטמון HTTP).
- תיעוד API וארכיטקטורה.
- גישה למאגר Git עם קוד מלא.
- הדרכת מפתחים שלך (עד שעתיים) ו-2 חודשים של תמיכה בדוא"ל.
ספר לנו על הפרויקט שלך — נעריך את ההיקף ונציע פתרון אופטימלי. הזמן פיתוח של קטלוג React והגבר את ההמרה של החנות שלך.

