אשכול סמנים: היכן המפה נשברת ומה הלקוח מפסיד
מפה עם עשרה סמנים עובדת באופן מיידי, ובשלב הזה נראה שהמשימה נפתרה. אבל ברגע שהמאגר גדל למאות ואלפי אובייקטים — האפליקציה מתחילה לגמגם באופן מורגש: הרינדור נחלש, המחוות מקוטעות, ובמכשירים חלשים המפה באפליקציה קופאת בכל תזוזה.
הסיבה אינה "טלפון חלש", אלא שכל אובייקט מרונדר כרכיב נפרד. ככל שיש יותר נקודות על המפה, כך כל פריים עולה יותר.
בהמשך מופיעה תקלה שנייה, פחות מובנת מאליה — אובדן נראות. כאשר הסמנים צפופים, הם מכסים זה את זה: המשתמש רואה אייקון אחד במקום שבו למעשה יש עשרים. האובייקט קיים פיזית בנתונים, אך על המסך הוא אינו מופיע. עבור תרחיש הלקוח זהו הפסד ישיר — הלקוח אינו מוצא את הנקודה הרצויה ועוזב, אף שהיא הייתה בקרבת מקום.
התקלה השלישית הפוכה בסימפטום, אך פוגעת באותה מידה בתוצאה — תחושת ריקות כוזבת. אם בעת הקטנת קנה המידה המפה אינה מרכיבה מחדש את הנתונים, חלק מהאובייקטים פשוט נעלמים מהתוצאות, והמשתמש מסיק שאין הצעות באזור שלו. למעשה הן קיימות, אך הממשק לא הציג אותן.
כל שלוש התקלות צומחות מאותו שורש: המפה מנסה להציג הכול בבת אחת, במקום להציג את הנדרש בקנה המידה הנוכחי. אשכול סמנים פותר בדיוק את זה — מקבץ אובייקטים קרובים לצומת אחד וחושף אותם ככל שמתקרבים. ביצועי המפה מפסיקים להיות תלויים במספר הנקודות הכולל, והצגת האובייקטים על המפה נשארת הוגנת בכל זום.
מפרקים כיצד זה נסגר בפועל ואילו פרמטרים של קיבוץ קובעים אם המפה תהיה מגיבה.
מה נותנת קיבוץ סמנים: האפקט במספרים ובמצבים
קיבוץ סמנים הוא לא "מפה יפה", אלא דרך לשמור על מהירות רינדור המפה בגבולות צפויים עם גידול במספר האובייקטים. בלעדיו, כל נקודה על המפה היא אלמנט ממשק נפרד: בכמה אלפי סמנים, גלילה ושינוי קנה מידה מתחילים לפגר אחרי המחווה, ובמכשירים חלשים המפה מגמגמת כבר בפתיחה.
קיבוץ נקודות על המפה הופך את התלות הזו לכמעט קבועה: מספר האובייקטים המרונדרים נקבע לא לפי כמות הנתונים, אלא לפי קנה המידה הנוכחי.
מה משתנה במספרים
- זמן רינדור פריים. בעשרות אלפי סמנים ללא קיבוץ, הפריים נבנה זמן רב noticeably יותר; לאחר ההטמעה — יציב בטווח של פריים-שניים בכל קנה מידה.
- זמן עד למפה אינטראקטיבית. המסך מפסיק לחכות לטעינה מלאה של כל הנקודות: תחילה מוצגים הקיבוצים, והפירוט נטען בהדרגה עם ההתקרבות.
- עלות חוסר הפעילות של המפה. בזמן שהמשתמש ממתין לתגובה, התרחיש לא מתבצע — ההזמנה מאבדת המרה. מפה מגיבה מחזירה את הפעולות הללו למשפך.
- צריכת זיכרון. במקום אלפי אובייקטים חיים על המסך — בודדים של קיבוצים, מה שמפחית את הסיכון לקריסת האפליקציה בסשן ארוך.
מצבי תצוגת המפה
המצב נבחר לפי התרחיש, ולא "כברירת מחדל": במקום מסוים חשובה הצפיפות, במקום אחר — הדיוק של סמן בודד. להלן — חלופות עבודה שבהן באמת בוחרים.
| אפשרות | מתי מתאימה | מגבלות |
|---|---|---|
| קיבוץ רשתי | נתונים הומוגניים צפופים, תרחיש פשוט | הקיבוצים "קופצים" בעת שינוי קנה מידה |
| קיבוץ היררכי | אובייקטים הטרוגניים, נדרש פירוט לפי רמות | הגדרת ספים מורכבת יותר |
| קיבוץ לפי אזורים | גאוגרפיה ידועה מראש, אזורים יציבים | עובד רע בגבולות לא ברורים |
| ללא קיבוץ | פחות ממאה סמנים, דיוק גבוה | ירידה בביצועים עם גידול בנתונים |
המהנדסים שלנו מגדירים ספים, התנהגות בחפיפה וחלקות מעברים בין רמות — Supercluster וספריות דומות נותנות בסיס, אך הפרמטרים עבור מערך נתונים ספציפי נבחרים ידנית.
למה זה משתלם
האפקט אינו בקיבוץ עצמו, אלא בכך שהמפה מפסיקה להיות צוואר בקבוק של התרחיש. המשתמש מגיע ליעד ללא הפסקות, והמוצר עומד בגידול מאגר האובייקטים ללא שכתוב המסך.
כיצד אנו מיישמים אשכול: שלבי עבודה הנדסית
יישום אשכול מתחיל לא בקוד, אלא בביקורת של מפת האפליקציה: בודקים כמה אובייקטים נטענים בפועל למסך, כיצד בנוי מקור הנתונים, היכן אובדת ההיענות בזום ובפנורמה.
בשלב זה מתעדים את המדדים הנוכחיים — זמן רינדור האזור, מספר הבקשות בו-זמנית, התנהגות ברשת חלשה — כדי שלאחר העבודה יהיה עם מה להשוות.
- ביקורת מפת האפליקציה. מפרקים את המימוש הנוכחי: כיצד נוצרים המרקרים, מהיכן מגיעות הקואורדינטות, האם יש כפילויות ונקודות "מתות". קובעים ספים שמעבר להם המפה מתחילה להאט, וערכי יעד למספר האובייקטים על המסך.
- תכנון סכמת הקיבוץ. בוחרים מודל אשכול בהתאם לאופי הנתונים: נקודות סטטיות, אובייקטים דינמיים או סט משולב. מתאמים התנהגות בקני מידה שונים — היכן המרקר נשאר בודד והיכן מתאחד לקבוצה.
- הגדרת קיבוץ מרקרים. מיישמים חלוקה לפי רשת עם חישוב מחדש בעת שינוי קנה מידה, מגדירים רדיוס איחוד ועדיפויות תצוגה. בנפרד מפתחים תוויות אשכול וחלקות מעבר מחדש, כדי שהמפה לא "תקפוץ" בזום.
- אופטימיזציה של טעינת נתונים. מעבירים את הטעינה לאזור הנראות: האפליקציה מבקשת רק את האובייקטים שנכנסים למסך, ומטמונת את אלו שכבר התקבלו. זה מסיר עומס מיותר מצד השרת ומאיץ את תגובת המפה.
- בדיקת המפה. מריצים תרחישים על כמויות נתונים שונות, ערוץ חלש והתקנים ישנים: זום, פנורמה, מעבר מאשכול לנקודות בודדות. משווים התנהגות לספים המוסכמים ומתקנים אי-התאמות.
- מסירת תיעוד. מוסרים ללקוח את הגדרות הקיבוץ, סכמת מקור הנתונים, תיאור הספים וההתנהגות בכל קנה מידה. מתעדים כיצד לשנות פרמטרים ללא מעורבות מפתח.
כל שלב מסתיים בארטיפקט שניתן לבדוק: דוח ביקורת, סכמה מוסכמת, אב-טיפוס עובד על נתונים אמיתיים, תוצאות בדיקה. סדר זה מסיר את המצב שבו האשכול מיושם, אך המפה עדיין מאטה באזורים צפופים — אי-ההתאמות נראות בשלב הבדיקה, ולא לאחר השחרור.
במה שונה קיבוץ בצד הלקוח מקיבוץ בצד השרת?
קיבוץ בצד הלקוח מחשב קבוצות ישירות על המכשיר: האפליקציה מחזיקה את כל הנקודות בזיכרון, מחשבת מחדש את הרשת בכל הזזה או זום ומציירת סמן אחד לכל תא.
קיבוץ בצד השרת מעביר את העבודה הזו ל-backend — האפליקציה שולחת את האזור הנראה ורמת הזום, ובתגובה מקבלת אשכולות מוכנים עם קואורדינטות ומונה. ההבדל מתבטא לא בתמונה, אלא במקום שבו נצרך המשאב ובמה שקורה כשהרשת נחלשת.
האפשרות בצד הלקוח מנצחת בסטים קטנים ובינוניים: עד כמה אלפי נקודות, חישוב מחדש של הרשת נכנס לפריים, המפה נשארת מגיבה ללא שאילתה אחת, ובחיבור חלש ההתנהגות בכלל לא משתנה — הנתונים כבר על המכשיר.
הגבול מגיע בנפח: עשרות אלפי סמנים אוכלים זיכרון, חישוב מחדש בכל תנועה מתחיל להפיל את קצב הפריימים, וההפעלה הקרה נמשכת עד שהסט כולו נטען. בנוסף, הנתונים נמצאים על הלקוח במלואם — זו שאלה לא רק של משקל, אלא של מה בכלל יוצא אל המכשיר.
קיבוץ בצד השרת מסיר את העומס מהאפליקציה: המכשיר מאחסן רק את מה שנכנס למסך, נפח הדגימה מוגבל לאזור הנראה, וסטים כבדים חיים ב-backend. המחיר — תלות בערוץ. כל הזזה של המפה הופכת לשאילתה, ובחיבור חלש המשתמש רואה מפה ריקה או טעינה במקום סמנים.
זה מטופל באמצעות מטמון של התגובות האחרונות לפי אריחים, איחוד שאילתות סמוכות וציור המצב הקודם עד שמגיעה תגובה חדשה — אבל זו כבר שכבה נפרדת של עבודה מעל ארכיטקטורת המפה.
בפועל, הבחירה לעתים רחוקות בינארית. לעתים קרובות נוקטים בהיבריד: שכבת אשכולות בסיסית מגיעה מהשרת, והפירוט בתוך אזורים צפופים מחושב בצד הלקוח מתוך הסט שכבר נטען.
הגבול בין הגישות עובר לפי נפח הנתונים, דרישות לרעננות הנקודות ועד כמה קריטית העבודה במטרו, בכביש או במרתף — כלומר במקומות שבהם החיבור נעלם.
מקרה בוחן: כיצד קיבוץ סמנים האיץ את המפה באפליקציית משלוחים
באפליקציית משלוחים המפה הציגה את כל ההזמנות הפעילות והשליחים בו-זמנית — בעומס צפוף של אזור, הוצגו על המסך אלפי נקודות ציון.
מדידות על מכשירים אמיתיים הראו זמן רינדור פריים של כ-1.2 שניות בעת תנועה (panning) ועד 2.5 שניות בעת זום: הממשק רעד, וחלק מהאובייקטים לא הספיקו להתרנדר לפני המחווה הבאה.
בעיה נפרדת — אובייקטים אבודים: בעת גרירה מהירה של המפה, סמנים שנכנסו לפריים ברגע הרינדור פשוט לא הופיעו, והדיספצ'ר ראה תמונה חלקית של האזור.
התחלנו בפרופיילינג: לקחנו צירי זמן של רינדור על מכשיר בינוני וחלש, חישבנו כמה סמנים באמת נכנסים לאזור הנראה בתרחישי עבודה טיפוסיים של הדיספצ'ר. התברר שעל המסך נכנסים פיזית 20–40 אובייקטים, אבל לשכבת הרינדור נשלחו כולם — והפער הזה היה מקור הירידה בביצועים.
מה שינינו
הכנסנו קיבוץ סמנים עם חישוב מחדש של הרשת בהתאם לקנה המידה הנוכחי: בזום רחוק נקודות קרובות מתקפלות לסמן אחד עם מספר, ובזום קרוב הן נפתחות חזרה לאובייקטים נפרדים.
החישוב מחדש מתבצע באופן אינקרמנטלי בעת שינוי אזור הנראות, ולא בכל פריים, ולכן תנועה (panning) כבר לא מפעילה בנייה מחדש מלאה של השכבה.
הוספנו סף פתיחה של אשכול ועדיפות תצוגה: הזמנות בעייתיות ושליחים פעילים נשארים גלויים יותר מאחרים. זה ביטל את התרחיש שבו סמן חשוב התחבא בתוך קבוצה.
מדדים לפני ואחרי
| מדד | לפני | אחרי |
|---|---|---|
| זמן רינדור פריים בעת תנועה (panning) | ~1.2 ש' | 40–60 מ"ש |
| זמן רינדור בעת זום | עד 2.5 ש' | 60–90 מ"ש |
| שיעור אובייקטים אבודים בעת הזזה מהירה | עד 15% | פחות מ-1% |
| מספר אובייקטים בשכבת הרינדור | כל הפעילים | 20–40 נראים |
החלקות חזרה במכשירים שבהם המפה קודם לכן הייתה בלתי שמישה לעבודה. הדיספצ'רים הפסיקו לחכות לתגובה בעת גרירה, ותמונה חלקית של האזור כבר לא הובילה להקצאות שגויות.
את לוגיקת הקיבוץ והחישוב מחדש של הרשת בנינו על יכולות שכבת המפות MapLibre — היא מספקת מקור נתונים נשלט ולא גוררת איתה משקל מיותר בבנייה של האפליקציה.
ספקות לפני הטמעה: תאימות, מיגרציה, חזרה לאחור, הרשאות
קיבוץ סמנים לעיתים רחוקות שובר את האפליקציה עצמה — הוא מתנגש עם מפה שכבר עובדת: שכבות של אחרים, סמנים מותאמים אישית, לוגיקת סינון והרשאות על מפתחות. לכן לפני ההתחלה אנחנו מגדירים את נקודות החיבור: איזו מפה משמשת כעת, כיצד מוגשות האריחים ומי owns מפתח הגישה.
מה אם המפה כבר משולבת ואי אפשר לשנות אותה? הקיבוץ מותקן מעל השכבה הנוכחית — הסמנים הקיימים, הפופ-אפים והמטפלים נשארים במקומם, משתנה רק אופן הרינדור כאשר יש ריכוז צפוף של נקודות. מיגרציה של המפה אינה נדרשת: החיבור מתבצע לאותה מפה שכבר נמצאת באפליקציה.
מה אם צריך לחזור להתנהגות הקודמת? החזרה לאחור של השינויים נעשית באמצעות מתג בקונפיגורציה: הקיבוץ מופעל ומכובה ללא בנייה מחדש של הגרסה, והלוגיקה הקודמת של הצגת הסמנים נשמרת כמצב גיבוי.
כיצד נפתרות הרשאות הגישה למפה ולמפתחות?
המפתחות והטוקנים נשארים בחשבון שלכם אצל ספק המפות — אנחנו לא מעבירים אותם לצד שלנו. ההרשאות ניתנות למשך תקופת העבודה ומבוטלות לאחר המסירה, וקונפיגורציית המפתחות מתוארת בתיעוד המפה: היכן נשמרת, כיצד מתבצעת רוטציה, ואילו הגבלות לפי דומיינים ואפליקציות הוגדרו.
לאחר המסירה הלקוח מקבל לידיו:
- קונפיגורציית קיבוץ: ספי איחוד, התנהגות בקני מידה שונים, סגנונות קיבוץ;
- תרשים תאימות המפה — אילו שכבות ומטפלים הושפעו, ואילו נותרו ללא שינוי;
- נוהל מיגרציה של המפה וסדר המעבר בין מצבי התצוגה;
- תהליך החזרה לאחור של השינויים עם בדיקה צעד-אחר-צעד לאחר החזרה;
- מטריצת הרשאות גישה למפה: מי owns המפתחות, למי ניתנה גישה, כיצד היא מבוטלת;
- תיעוד המפה עם תיאור הקונפיגורציה, ההגבלות ונקודות ההרחבה.
תארו את המשימה שלכם — נחזור עם הערכה ותוכנית עבודה עבור האפליקציה שלכם.







