Streaming Structured UI — הצגת ממשק בזמן שהמודל עדיין עובד

מאת צוות מדיה דיל · 12.08.2026 · Generative UI · 5 דק׳

מדריך ל-Streaming Structured UI: איך מרנדרים ממשק תקין בזמן שמודל עדיין מייצר את הפלט, פרסינג JSON חלקי, סדר הגעת שדות והשפעה על Core Web Vitals.

סוכן AI מנתח מסמך פיננסי ארוך ומכין דוח סיכום עם טבלה, גרף וכמה תובנות טקסטואליות. אם המשתמש רואה מסך ריק במשך שמונה שניות ואז את כל הדוח מופיע בבת אחת, החוויה מרגישה איטית ומנותקת — גם אם זמן העיבוד הכולל זהה למקרה שבו הטבלה מופיעה תוך שנייה, השורות מתמלאות בהדרגה, והתובנות נכתבות מילה-מילה בזמן שהמודל עדיין מייצר אותן. Streaming Structured UI הוא הפרקטיקה של רינדור ממשק חלקי ותקין בזמן שהמודל עדיין "בונה" את הפלט המלא — לא רק סטרימינג טקסט, אלא סטרימינג של מבנה נתונים שמתעדכן ומתמלא לאורך זמן, כשכל שלב ביניים הוא בעצמו ממשק תקין ולא רק גרסת ביניים שבורה.

למה סטרימינג טקסט לא מספיק

סטרימינג טוקן-אחר-טוקן פתר כבר מזמן את בעיית ה"המתנה השחורה" בצ'אטים טקסטואליים — במקום לחכות לתשובה מלאה, רואים אותה נכתבת בזמן אמת. אבל ברגע שהפלט הוא לא פרוזה חופשית אלא JSON מובנה (כפי שדנו בו במדריך UI as Data), סטרימינג תו-אחר-תו הופך לבעיה: אי אפשר לרנדר "חצי אובייקט JSON" — התחביר פשוט לא תקין עד שהסוגריים נסגרים. הפתרון הנפוץ הוא parser חלקי (partial JSON parser) שיודע לפרש מבנה לא-שלם ולהחזיר את מה שכבר תקין ממנו, תוך התעלמות זמנית משדות שעדיין באמצע כתיבה.

זה משנה את חוזה הרינדור: קומפוננטה שמציגה כרטיס מוצר צריכה לדעת להתמודד עם מצב שבו יש לה כותרת אבל אין עדיין מחיר, או שיש רשימת תכונות עם שני פריטים שעדיין הולכת ומתארכת. זו לא תוספת קטנה — היא דורשת שכל קומפוננטה בממשק תתוכנן מראש עם מצבי "חלקי" מפורשים, ולא רק מצב "מלא" ומצב "טעינה" בינארי כמו שרוב הממשקים בנויים היום.

עיצוב קומפוננטות ל-Partial Rendering

קומפוננטה שתומכת ברינדור חלקי צריכה סוג חדש של props: לא רק הנתונים עצמם, אלא גם מידע על אילו שדות עדיין "בתהליך" לעומת "סופיים". לדוגמה:

type StreamingCardProps = {
  title?: string;
  titleComplete: boolean;
  price?: number;
  priceComplete: boolean;
  features: string[];
  featuresComplete: boolean;
};

עם המידע הזה, הקומפוננטה יכולה להציג skeleton עדין רק בשדות שעדיין לא הושלמו, בעוד שדות שכבר סופיים מוצגים במלואם — במקום skeleton גורף על הכרטיס כולו שנעלם בבת אחת כשהכול מוכן. זה מקביל ישירות לעיקרון שמאחורי רינדור concurrent ב-React: לחלק עבודה למקטעים קטנים שכל אחד יכול להסתיים באופן עצמאי, במקום לחסום את כל העץ עד שהכול מוכן יחד.

סדר ההגעה: מה משדרים קודם

החלטה ארכיטקטונית חשובה היא באיזה סדר לבקש מהמודל למלא את השדות. אם השדה הראשון שמגיע הוא הכי חשוב ויזואלית — כותרת, תמונה ראשית — המשתמש מקבל תחושת התקדמות אמיתית תוך רגע. אם המודל בוחר סדר לוגי משלו (למשל ממלא קודם שדות פנימיים לא מוצגים כמו metadata), המשתמש ימשיך לראות מסך ריק גם אחרי שהמודל כבר "עובד". הפתרון הוא לתכנן את הסכימה עם רמזי עדיפות מפורשים — סדר השדות בסכימה עצמה, או הנחיה מפורשת בפרומפט לגבי סדר המילוי — כך שהמודל מתבקש לייצר תחילה את מה שהכי משמעותי לתפיסת ההתקדמות של המשתמש, ורק אחר כך פרטים משניים.

גם סדר הרינדור בצד הלקוח משנה: קומפוננטות שממתינות לנתון שעדיין לא הגיע לא צריכות לחסום קומפוננטות אחיות שכבר מוכנות. זה דורש מודל רינדור שמאפשר לכל "ענף" בעץ הממשק להתקדם בקצב שלו — ממש כמו שדפי אינטרנט בנויים בגישת Islands Architecture מאפשרים לכל אזור בעמוד להיטען ולהתעדכן באופן עצמאי בלי לחכות לשאר העמוד.

טיפול בשגיאות באמצע סטרימינג

תרחיש שקל לפספס בתכנון ראשוני הוא מה קורה כשהמודל נכשל באמצע ייצור מבנה — חיבור נקטע, quota נגמר, או שהפלט עצמו מגיע פגום ולא ניתן להשלמה תקינה. ברינדור לא-סטרימינג התשובה פשוטה: פשוט לא מציגים כלום עד שיש תשובה שלמה ותקינה. בסטרימינג המצב מורכב יותר, כי המשתמש כבר ראה חלק מהממשק — כרטיס עם כותרת אבל בלי מחיר שלא ישלים לעולם. הפתרון הנכון הוא מצב שגיאה ברמת השדה, לא רק ברמת הרכיב כולו: השדות שכבר הושלמו נשארים גלויים ותקינים, והשדות שנתקעו מקבלים אינדיקציה ברורה של כשל עם אפשרות ניסיון חוזר ממוקד, במקום לזרוק את כל מה שכבר הצליח להיווצר רק כי חלק אחד נכשל.

כדאי גם להגדיר timeout מפורש לכל שדה שממתין להשלמה, ולא רק timeout גלובלי לבקשה כולה. שדה שלא הושלם תוך פרק זמן סביר — למשל שלוש שניות — צריך לעבור למצב "נכשל" באופן יזום, כדי שהמשתמש לא ימשיך להביט בשלד טעינה שלעולם לא יתמלא בפועל.

ביצועים נתפסים מול ביצועים אמיתיים

סטרימינג מבני לא מקצר את זמן העיבוד הכולל של המודל — הוא משפר את התפיסה הסובייקטיבית של המשתמש לגבי כמה זמן חלף, וזה בעל ערך אמיתי, אבל צריך למדוד אותו נכון. מדד Time to First Byte הרגיל לא מספיק כאן; המדד הרלוונטי הוא Time to First Meaningful Render — כמה זמן עד שהמשתמש רואה משהו שכבר נותן לו מידע שימושי, לא רק שלד ריק. חשוב גם לוודא שהתחושה החיובית הזו לא באה על חשבון Core Web Vitals אמיתיים: עדכוני DOM תכופים מדי בזמן סטרימינג עלולים לפגוע ב-Interaction to Next Paint אם הם חוסמים את ה-Main Thread. הפתרון המעשי הוא לתזמן עדכוני רינדור בקצב מבוקר (למשל דרך requestAnimationFrame או batching מפורש) כדי שהאשליה של תגובתיות מלאה לא תבוא על חשבון תגובתיות אמיתית לקלט משתמש.

מתי סטרימינג מבני לא משתלם

כמו כל דפוס שמוסיף מורכבות, גם Streaming Structured UI לא מתאים לכל תרחיש. עבור פלטים קטנים שמסתיימים תוך פחות משנייה, המורכבות הנוספת של parser חלקי, מצבי ביניים לכל שדה, וטיפול בשגיאות ברמת שדה פשוט לא מצדיקה את עצמה — המשתמש בקושי מספיק להבחין בהבדל בין הצגה מיידית להצגה סטרימינגית כשהכול קורה כל כך מהר. הכלל המעשי הוא למדוד קודם: אם זמן היצירה הטיפוסי של הפלט המלא עולה על סף מסוים (למשל שנייה וחצי), ההשקעה בסטרימינג משתלמת; מתחת לזה, עדיף להשקיע את אותו מאמץ הנדסי בקיצור זמן העיבוד עצמו, לא בהסתרתו מאחורי אנימציה.

תגיות: Streaming UI · Partial JSON · Generative UI · Concurrent Rendering · Core Web Vitals · Islands Architecture

← חזרה לבלוג · צור קשר