באתר Next.js אפשר להציג קטלוג עשיר עוד לפני שהדפדפן סיים להפעיל את כל הקוד. באותו עמוד אפשר גם לסנן תוצאות תוך כדי הקלדה, לפתוח חלונית פרטים ולהמשיך לכתובת אחרת בלי טעינה מלאה. היכולות האלה אינן מגיעות מכך שהכול רץ בשרת או מכך שהכול רץ בדפדפן. הן תוצאה של חלוקה בין שתי הסביבות.
החלוקה נעשית חשובה כשאתר גדל מעבר לעמוד תצוגה. יש מידע ציבורי שאפשר להכין מראש, נתונים שצריכים להתעדכן ופעולות שמגיבות מיד לבחירה של המשתמש. כדי לתכנן את החיבור ביניהם צריך להבין מה פירוש Server Component ומה באמת עושה ההוראה use client.
איך React ו־Node.js משתלבים ב־Next.js
React היא ספרייה לבניית ממשקים מרכיבים. Next.js היא מסגרת עבודה שמוסיפה בין היתר ניתוב, רינדור וכלים לעבודה עם נתונים. Node.js היא סביבת הרצה של JavaScript מחוץ לדפדפן ומשמשת כברירת המחדל של Next.js בצד השרת. אלה חלקים שיכולים לעבוד יחד ולא שלוש חלופות לאותה החלטה.
JavaScript היא השפה שבבסיס המערכת. TypeScript מוסיפה לה מערכת טיפוסים שעוזרת לתאר נתונים ולזהות שגיאות בזמן הפיתוח. היא אינה סביבת הרצה נוספת שהמבקר צריך להתקין. מי שרוצה להעמיק בהבחנות האלה יכול להמשיך למדריך על שפות וטכנולוגיות לפיתוח אתרים.
המשך המאמר מתייחס ל־App Router של Next.js. בפרויקטים שמשתמשים ב־Pages Router פוגשים דפוסי עבודה אחרים. לכן כשפותחים פרויקט קיים כדאי לזהות תחילה באיזה נתב הוא משתמש, ולא להעביר אליו דוגמה שנכתבה למבנה אחר בלי להבין את ההבדל.
רכיב שרת אינו הבטחה לחישוב מחדש בכל ביקור
ב־App Router עמודים ופריסות הם Server Components כברירת מחדל. הקוד שלהם יכול לקרוא נתונים ממסד נתונים או משירות פנימי בזמן הרינדור בשרת. אין צורך להקים נתיב API ציבורי רק כדי שרכיב שרת יקבל מידע שכבר נגיש לו באותה סביבה.
הקוד של רכיב השרת אינו נשלח לדפדפן כדי לרוץ שם. התוצאה שלו כן מגיעה למבקר. Next.js משתמשת בנתונים שנקראים RSC Payload כדי לתאר את עץ הרכיבים ולשלב אותו עם רכיבי הלקוח. בכניסה ישירה לעמוד נוצרת גם תצוגת HTML ראשונית.
חשוב להפריד בין השאלה איפה הקוד יכול לרוץ לבין השאלה מתי מפיקים את התוצאה. רכיב שרת יכול להשתתף בתוכן שהוכן בזמן בניית הפרויקט או בתוכן שמחושב בזמן בקשה. מדיניות המטמון והמידע שהרכיב צורך משפיעים על ההחלטה. עצם השם Server Component אינו אומר שכל ביקור מפעיל מחדש כל שאילתה.
use client אינו אומר שהעמוד מתחיל ריק
רכיב שמשתמש במצב משתנה של React או במטפלי אירועים כמו לחיצה צריך קוד שיכול לרוץ בדפדפן. מסמנים את נקודת הכניסה שלו באמצעות use client בתחילת הקובץ. כך Next.js יודעת שהרכיב והקוד שהוא מייבא שייכים גם לצד הלקוח.
בכניסה הראשונה לכתובת, Next.js יכולה להפיק HTML גם מרכיבי לקוח. לאחר הגעתו לדפדפן, React מחברת אליו את ההתנהגות באמצעות hydration. לכן Client Component אינו מילה נרדפת לעמוד שממתין ל־JavaScript לפני שמופיע בו טקסט כלשהו. במעבר פנימי בין עמודים תהליך העדכון שונה מכניסה ישירה, והוא משתמש גם ב־RSC Payload.
הגבול נקבע לפי קשרי הייבוא של הקוד ולא רק לפי מה שנמצא חזותית בתוך רכיב אחר. אם רכיב לקוח מייבא רכיב נוסף, הייבוא מצטרף לצד הלקוח. לעומת זאת, רכיב שרת יכול להכין תוכן ולהעביר את התוצאה כ־children לרכיב לקוח. במקרה הזה התוכן מופיע בתוכו בלי להעביר אליו את קוד השרת שיצר אותו.
זו הסיבה שכדאי להציב את הגבול קרוב לאינטראקציה. שדה חיפוש חי צריך מצב מקומי. כותרת הספרייה ותיאור הנושאים שסביבו אינם צריכים להפוך לרכיבי לקוח רק מפני שהם מופיעים באותה פריסה. גבול רחב מדי עלול לשלוח לדפדפן קוד שאין לו תפקיד בשימוש השוטף.
מתכננים קטלוג ידע לפי סוג הפעולה
נניח שבונים ספריית מדריכים ציבורית. לכל מדריך יש כותרת, נושא ותאריך עדכון. הקוראים יכולים לסנן לפי נושא ולפתוח עמוד מלא. בהמשך מתווספת גם רשימת מדריכים שנשמרו לקריאה בחשבון אישי. לא נכון לתת לכל החלקים האלה אותה דרך טעינה.
את כותרת הקטלוג ואת הנתונים הציבוריים הראשוניים אפשר להביא בשרת. רכיב החיפוש יקבל רק את השדות הדרושים להצגה ולסינון. אם מדובר ברשימה קטנה, סינון מקומי יכול לתת תגובה מיידית בלי בקשה נוספת לכל אות. המידע כבר נמצא בדפדפן ולכן אין צורך לבקש אותו שוב.
ברשימה גדולה התמונה משתנה. שליחת הקטלוג כולו לכל מבקר מגדילה את ההעברה ואת העבודה בדפדפן. במקרה כזה אפשר לבצע חיפוש בצד השרת ולהחזיר מקטע תוצאות מתאים. אם רוצים לשתף את מצב החיפוש או לחזור אליו באמצעות כפתור החזרה, שומרים את הבחירות המתאימות בכתובת ולא רק במצב זמני של הרכיב.
עמוד המדריך עצמו יכול להישאר ברובו תוכן שרת. חלונית שמציגה הסבר נוסף או כפתור שמעדכן בחירה יכולים להיות רכיבי לקוח קטנים. אין צורך להפוך את כל המדריך לאפליקציה דפדפנית בגלל פעולה אחת. מצד שני, אם רוב המסך הוא עורך אינטראקטיבי, חלוקה רחבה יותר בצד הלקוח יכולה להיות הגיונית.
גם התנהגות מובנית של הדפדפן חשובה בהחלטה. אזור details יכול להיפתח ולהיסגר בלי לוגיקת מצב של React. קישור רגיל יכול להוביל למדריך אחר. לא כל דבר שאפשר ללחוץ עליו מחייב Client Component. רכיב לקוח נחוץ כאשר ההתנהגות באמת תלויה בקוד ובמצב שפועלים בדפדפן.

הנתונים שעוברים לדפדפן הם חלק מהתכנון
העברת props מרכיב שרת לרכיב לקוח אינה מעבר פנימי שנשאר מוסתר בשרת. הנתונים עוברים סריאליזציה ומגיעים לדפדפן. לכן כדאי להגדיר עבור קטלוג הידע מבנה תצוגה קטן: מזהה, כותרת, נושא וכתובת. אין צורך להעביר אובייקט מלא של מסד הנתונים אם הממשק משתמש רק בארבעה שדות.
הערכים צריכים להיות ניתנים לסריאליזציה לפי הכללים של React. אובייקט תצוגה פשוט עם טקסט ומספרים קל להבנה ולתחזוקה. חיבור למסד נתונים אינו נתון תצוגה. גם פונקציית טיפול רגילה בלחיצה אינה דבר שמעבירים מהשרת לדפדפן כאילו היא טקסט; יש להגדיר את ההתנהגות במקום המתאים.
מפתחות גישה ופרטי חיבור נשארים בצד השרת. עצם השימוש ברכיב שרת אינו מונע ממפתח להעביר בטעות שדה שאינו מיועד להצגה. ההפרדה צריכה להופיע גם בשכבת הנתונים: איזה מידע מותר להחזיר לקטלוג הציבורי ואיזה מידע קשור למשתמש מסוים. אפשר לסמן מודולים שמיועדים לשרת בלבד כדי לזהות ייבוא שלהם למקום שגוי בזמן הבנייה.
הרשימה האישית שבדוגמה דורשת טיפול נפרד מהקטלוג הציבורי. המערכת צריכה לבדוק את המשתמש ואת ההרשאות בצד השרת ולהחזיר רק את הנתונים המתאימים לו. הסתרת כפתור בממשק אינה מחליפה את ההחלטה הזאת. גם מטמון משותף אינו מקום שאליו מעבירים אוטומטית פלט אישי.
רעננות הנתונים אינה נקבעת לפי המילה Server
נניח שעורך עדכן כותרת של מדריך. מתי השינוי צריך להגיע לקוראים? זו שאלה שצריך לענות עליה במפורש. תוכן ציבורי שמתעדכן לעיתים רחוקות יכול להתאים לשמירת תוצאה במטמון. נתון תפעולי שמשתנה ללא הפסקה עשוי לדרוש קריאה טרייה יותר. שני הסוגים יכולים להופיע באותו עמוד.
בגרסאות הנוכחיות של Next.js יש משמעות גם לשאלה אם הופעל Cache Components בפרויקט. המודל הזה מופעל באמצעות ההגדרה cacheComponents וכולל שימוש ב־use cache וב־cacheLife כדי להגדיר תוצאות שנשמרות ומשך שימוש בהן. פרויקט שלא הפעיל אותו עובד לפי מודל אחר. לא כדאי לערבב הגדרות משני המודלים מתוך הנחה שכולן חלות תמיד.
צריך להבחין גם בין מניעת קריאות כפולות בתוך רינדור אחד לבין שמירת נתונים לבקשות עתידיות. שתי פונקציות יכולות לקבל אותה תוצאה במהלך בקשה בלי שהנתונים יישמרו לשעה או ליום. React.cache משמשת לשיתוף תוצאה בתחום הבקשה; זו אינה הבטחה למטמון מתמשך לכל הקוראים.
עבור הקטלוג אפשר להגדיר מה מרענן את הנתונים הציבוריים: חלוף זמן מתאים או אירוע עריכה. אחרי שינוי תוכן צריך להבין איזה עותק מתרענן ואיזה עדיין קיים. ייתכן שהשרת כבר החזיר תוצאה חדשה אבל רכיב בדפדפן ממשיך להציג מידע ששמרה ספריית נתונים מקומית.
לכן בבדיקת עדכון פותחים גם כתובת ישירה וגם מעבר פנימי מתוך האתר. בודקים שהכותרת השתנתה במקום הצפוי ושחזרה לעמוד אינה מחזירה מצב שאינו מתאים. מדיניות המטמון היא חלק מהתנהגות המוצר. היא אינה רק מתג טכני שמפעילים כדי שהאתר יהיה מהיר יותר.
Streaming מאפשר לחלק את ההמתנה
בספריית המדריכים יש כותרת שכמעט אינה משתנה ולצדה אזור שמציג עדכונים אחרונים. אם בקשת העדכונים איטית, אין הכרח לעכב בגללה את כל מה שסביבה. אפשר להציב גבול Suspense סביב החלק שממתין ולהציג עבורו מצב ביניים בזמן שיתר העמוד מגיע.
המיקום של ההמתנה חשוב. אם רכיב האב ממתין לכל הנתונים לפני שהוא מחזיר את המבנה שלו, רכיב טעינה שמוגדר עמוק יותר לא יכול להציג חלק מהאב שכבר נחסם. לכן מקרבים את הקריאה האיטית לאזור שתלוי בה. כך החלוקה לקוד תואמת את החלוקה שהקורא רואה.
Suspense אינו הופך בקשה אטית למהירה ואינו יוצר מטמון בפני עצמו. הוא מאפשר להגדיר מה רואים בזמן שמחכים. מצב הביניים צריך לשמור על מקום סביר לתוצאה ולהסביר איזה חלק עדיין נטען. אוסף הבהובים ושינויים בגובה יכול להפוך הזרמה הדרגתית לחוויה פחות נוחה מהמתנה אחת ברורה.
כששתי קריאות נתונים אינן תלויות זו בזו, אפשר להתחיל אותן במקביל. אם רשימת הנושאים ורשימת המדריכים נפרדות, אין סיבה להמתין לראשונה לפני התחלת השנייה. אבל אם קריאה אחת זקוקה למזהה שהשנייה מחזירה, התלות אמיתית. חלוקה למספר רכיבים אינה מבטלת אותה.
Hydration דורשת התחלה עקבית
הדפדפן צריך לחבר התנהגות לתצוגה הראשונית שהגיעה מהשרת. אם הרינדור הראשון שלו מייצר תוכן שונה, עלולה להופיע אי־התאמה בזמן hydration. זה קורה למשל כשערך אקראי או חישוב זמן מפיקים שתי תוצאות שונות, או כשהממשק מניח שכבר קיימת העדפה שנשמרה רק בדפדפן.
בקטלוג הידע כדאי להתחיל מאותו מצב סינון שהגיע מהשרת. לאחר מכן אפשר לשלב העדפה מקומית כשסביבת הדפדפן זמינה. אם מידע צריך להיות משותף לכתובת ולתצוגה הראשונית, מגדירים מי אחראי עליו ולא נותנים לכל צד להחליט בנפרד.
הוראת use client אינה מאפשרת לקרוא ללא הבחנה לכל API של הדפדפן בזמן הרינדור הראשוני. Client Components משתתפים גם ברינדור השרת בכניסה ישירה. פעולות שתלויות ב־window או באחסון מקומי צריכות להתבצע בשלב ובסביבה שבהם הם קיימים.
לפני פרסום בודקים את גרסת הייצור ולא רק את שרת הפיתוח. נכנסים ישירות למדריך פנימי ומנסים גם מעבר אליו דרך הקטלוג. בודקים את התוכן הראשוני ואת ההתנהגות אחרי שהעמוד נעשה אינטראקטיבי. אם המטרה היא גם גילוי בחיפוש, המדריך על רינדור תוכן והופעת עמודים בחיפוש מדגים למה בדיקת כתובת פנימית חשובה.
שאלות על החלוקה בין השרת לדפדפן
האם כל Server Component חייב לרוץ על שרת Node.js קבוע?
לא. Node.js היא סביבת ברירת המחדל, אבל צורת הפריסה יכולה להשתנות בין שרת מנוהל לבין פלטפורמות אחרות. יש גם תוכן שאפשר להכין מראש. צריך לבדוק שהתשתית תומכת בתכונות שבהן משתמשים; סביבת Edge אינה מספקת את כל היכולות של Node.js.
האם Client Component פוגע בהכרח בקידום האתר?
לא. גם רכיב לקוח יכול לתרום HTML לתגובה הראשונית. השאלה היא אם התוכן החשוב באמת נוצר ונגיש בכתובת. מידע שמופיע רק אחרי פעולה אינו שקול לתוכן שמופיע מיד. הבחירה בפלטפורמה היא רק חלק מהתמונה, כפי שמוסבר בהשוואת כלים לבניית אתרים עם AI.
האם צריך להעביר את כל האתר לצד הלקוח כדי להוסיף חיפוש חי?
לא. אפשר להפריד את החיפוש ואת התוצאות שהוא מנהל מרכיבי התוכן שסביבו. גודל הקטלוג ותדירות העדכון יקבעו אם לסנן נתונים שכבר הגיעו לדפדפן או לבקש תוצאות מהשרת. נקודת הפתיחה היא התנהגות החיפוש הדרושה ולא שינוי גורף של כל הפריסה.



