ToCode
1.42K subscribers
3.84K links
טיפים קצרים למתכנתים מאת ינון פרק
Download Telegram
שלושה טריקים ששיפרו לי משמעותית את ציון הביצועים ב Page Speed
אחרי משחקי הביצועים אתמול חשבתי שיהיה מעניין לבדוק גם את הבלוג שלי פה בטוקוד ולראות מה Page Speed חושב עליו והאם אפשר לשפר. האמת שבאתי מאוד אופטימי לבדיקה כי בראש חשבתי שמדובר בסך הכל באתר סטטי כמעט בלי JavaScript ושרת שרוב הזמן מגיב מהר. לא נעים לשתף מה Page Speed חשב עליו, אבל בואו רק נגיד שזה לא תאם לציפיות. מסתבר שלאורך השנים הוספתי לאתר פיצ'רים קטנים בלי לחשוב על ההשפעה שלהם על זמני הטעינה, ובלי לחשוב עד הסוף אם אני מוסיף אותם בדרך הטובה ביותר. הטעויות הקטנות האלה מצטברות ומשפיעות.

אלה שלושת הטריקים שיישמתי כדי לשפר משמעותית את ציון ה Page Speed שעכשיו עומד על 94 לדף של פוסט מהבלוג:

טעינת נכסים סטטיים דרך Apache
האתר רץ על Rails וההמלצה באתרי ריילס היא לא להעמיס על השרת עם נכסים סטטיים (כלומר קבצי תמונות, js, css וכן הלאה). מספיק שבגלל עומס על השרת ייקח לו 3 שניות להיזכר שהוא צריך לשלוח קובץ CSS והאתר נטען לאט אצל מישהו. שרתי Apache ו nginx או CDN-ים למיניהם הרבה יותר טובים בשליחת קבצים סטטיים לגולשים, לכן הטריק הראשון היה להוריד משרת האפליקציה את המשימה הזאת ולהעביר אותה לשרת ה Apache שעומד לפניו.

השהיית טעינת JavaScript עד שנצטרך אותו
קופסת ה Comments שנמצאת פה מתחת לכל פוסט מגיעה מ disqus וה JavaScript שלהם הוא פח. נגן הוידאו שמשתמשים בו כדי לראות את הסרטים בקורסים מגיע מ Spotlightr ויש גם כמה אלמנטים של UI שמגיעים מספריית flowbite. את כל אלה העברתי לטעינה מושהית כך שייטענו רק כשנצטרך אותם, כלומר אם גולש באמת קורא את הפוסט וגולל עד למטה את העמוד אז נטען את תיבת ה Comments, אחרת אין טעם לטעון את ה JS שלהם. אם יש בעמוד סרט אפשר לטעון את הנגן של ספוטלייטר, אבל בטח לא צריך לטעון אותו בכל דף.

השינוי הזה מאוד שימח את Page Speed שמודד כמה זמן מעבד ה JavaScript של העמוד לקח.

מחיקת JavaScript שלא צריך
בהמשך להשהייה שמתי לב שיש גם ספריות JS שפעם השתמשתי בהן והיום כבר לא. הבולטת בהן היא Google Analytics ממנה נפרדתי לטובת Simple Analytics, אבל היו עוד כמה ספריות של UI שלא היה צריך. מחיקה של JavaScript תמיד משמחת את Page Speed וכך קיבלתי אצלם עוד כמה נקודות.

נקודות להמשך
אחרי השיפור בדפי הבלוג העבודה עדיין רחוקה מלהסתיים. יש עדיין קבצי JavaScript שאני חושב שאפשר לוותר עליהם, ה Page Speed חושב שעדיין יש לי יותר מדי CSS ואפשר לקצץ גם שם ועוד לא נגעתי בעמוד הבית או בדפי הקורסים. ועדיין יצאתי מהמשחק מחוזק ואופטימי לגבי שיפורים שעוד אפשר יהיה ליישם בהמשך.

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

לפעמים המשולש הזה עוזר לנו להבין את המציאות.

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

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

"מצד אחד אני רוצה לבנות מוצר יותר מהר, אבל מצד שני לא רוצה להתפשר על איכות הקוד".

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

כלל אצבע טוב בסיפור הזה הוא לא להניח מראש שיש בחירה בין אילוצים שונים. לפעמים באמת צריך לבחור שניים, אבל עדיף שזו לא תהיה ברירת המחדל.
1
ה fix השני מיותר
דילן הואנג החליט לסגור את הסטארט-אפ שלו ופרסם את כל הקוד שהם כתבו בגיטהאב כדי שדברים לא ילכו לפח. אני אוהב כאלה הזדמנויות כי זה מאפשר הצצה לפיתוח של מערכת אמיתית, לחצים אמיתיים וקוד אמיתי שנכתב.

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

f1ed4259c fix
75e825f62 fix
c388772ec remove unnecessary env group


1cc57ce33 fix sizing of logo
d26a25c47 fix sizing


f510e4f5d update
89007aad9 update
0d9701a14 updates to workflow


הרבה מהקומיטים, או יותר נכון הודעות הקומיט, מופיעות שוב ושוב. בדוגמה הראשונה אני מדמיין שאחרי שהוא הוריד את ה env group המיותרים היתה איזו תקלה וה fix מתייחס לתיקון אותה תקלה, ואולי התיקון לא הצליח מספיק טוב בפעם הראשונה אז מיד אחריו היה fix נוסף שמתקן את התיקון. מוכר? ברור. אבל יש גם דרך יותר טובה. ננסה את זה.

יצרתי ריפו ריק ובתוכו רק את שלושת הקומיטים כך שהלוג הוא:

41a1948 fix
0a197d4 fix
e7572cf remove unnecessary env group
daf174b initial commit



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

$ git reset HEAD~3
Unstaged changes after reset:
M textfile.txt

$ git add .
$ git commit -m 'remove unnecessary env group'


הפרויקט כעת נמצא באותו מצב של קומיט 41a1948 אבל הלוג הוא:

622a0da remove unnecessary env group
daf174b initial commit


ומי שיסתכל בלוג יראה בתוך קומיט 622a0da את כל השינויים של כל שלושת הקומיטים המקוריים.

נ.ב. יש מערכות שמכריחות אותך לעשות Push כדי להפעיל CI Pipeline. שינוי קומיטים אחרי דחיפה הוא תהליך הרבה פחות ידידותי ויכול לבלבל מפתחים אחרים שכבר משכו את הקומיטים הקודמים. במצבים כאלה אני אוהב ליצור ענף נפרד לצורך הפעלת ה CI ואז אחרי שסיימתי לבדוק אני ממזג את הענף ומוחק את הקומיטים המיותרים עם ריבייס.
אלא אם כן אתם נטפליקס
כשגולש נכנס אליכם לאתר, כמה זמן הדפדפן שלו צריך לעבוד ולהריץ JavaScript לפני שהוא יכול להשתמש באתר? מסתבר שבממוצע התשובה תלויה הרבה יותר בטכנולוגיה איתה בחרתם לבנות את האתר מאשר בפיצ'רים של האתר עצמו. הלכתי ל Page Speed לבדוק 9 אתרי ריאקט ו next.js ולא הופתעתי מהתוצאות:

1. בשביל לקרוא מאמר מהאתר של vercel צריך להריץ JavaScript במשך 2.5 שניות.

2. דף הבית של nike העסיק את הדפדפן במשך 8.1 שניות רק בהרצת הקוד.

3. דף הבית של solana עם הסלוגן Poweful for developers, Fast for everyone בילה "רק" 1.6 שניות בהרצת קוד.

4. לפני שבוחרים ארוחה ב wegmans הדפדפן צריך לבלות 10 שניות בהרצת ה JavaScript שלהם.

5. לפני שבוחרים קורס באקדמיה של קאן צריך לחכות 2.9 שניות כדי להריץ את ה JavaScript אצלם בעמוד.

6. לפני שאפשר לקרוא עדכונים ברדיט נקדיש 2.3 שניות להרצת JavaScript.

7. בקורסרה נבלה 3.8 שניות בהרצת ה JavaScript לפני שנוכל להיכנס לקורסים.

8. יודמי טיפה יותר טובים עם 3.1 שניות זמן ריצה של JavaScript בכניסה לעמוד.

צריך להגיד - משתמשים לא רוצים לבלות זמן בהמתנה ל JavaScript שירוץ. אם אפשר היה לבנות את האתר בצורה שתתן את אותה פונקציונאליות אבל בלי 10 שניות של זמן ריצה של JavaScript כולם היו שמחים יותר. מה שמשותף לכל האתרים ברשימה הוא הבחירה ב React, חלקם ישירות וחלקם גם עם next.js. כן יהיה מעניין לבדוק גם פריימוורקים אחרים אבל לא זאת הנקודה כאן. בואו נסתכל על עוד שני אתרים מעניינים:

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

אתר שני הוא נטפליקס, וידעתם שהוא יבוא. נטפליקס יודעים מה הם עושים ומוכנים להשקיע בתהליכי פיתוח, אופטימיזציות ומה לא. עמוד ה jobs שלהם בכתובת jobs.netflix.com משתמש בריאקט בצורה מאוד אפקטיבית ומבלה רק 0.6 שניות בהרצת JavaScript כשהדף נטען. אגף גם אתר התיעוד של ריאקט מאוד יעיל ומגיע לתוצאות דומות ואף טובות יותר.

הסיפור עם הפריימוורק הוא לא שכל דף ריאקט או next הולך להיטען לאט או לבלות המון זמן בהרצת JavaScript. אם אתם יודעים מה אתם עושים ומוכנים להתייחס לבעיות ביצועים כמו לכל באג אחר אפשר לקבל זמני טעינה טובים למרות ריאקט. מילת המפתח היא "למרות".
דוגמת קוד: בדיקת טופס עם yup ב vue
הדרך הכי קלה היום לעבוד עם טפסים היא לדלג עליהם בספריית ה SPA שלנו, ולהשתמש במאפייני ה HTML ו ValidityState של רכיבי הקלט כדי להציג הודעות למשתמשים. ועדיין לפעמים אנחנו צריכים בדיקות יותר מסובכות או שרוצים להריץ תהליך יותר מסודר סביב הנושא של בדיקות קלט בצד לקוח. במצבים כאלה נוכל להשתמש בספריה כמו yup כדי לתאר את הטופס ובקוד לעדכן את הודעות השגיאה בתוך אפליקציית vue או ריאקט. בואו נראה איך זה עובד.

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

import {object, string} from 'yup';

export const signupFormSchema = object({
email: string()
.email()
.required(),

password: string()
.min(8, 'password must be at least 8 characters long')
.required(),
});


סכימה של yup יודעת לקבל אוביקט ולהגיד אם הוא מתאים או לא בעזרת פונקציה אסינכרונית בשם validate. לדוגמה הקוד הבא:

try {
await signupFormSchema.validate({email: 'ynon@demo.com', password: '12345678'});
console.log('OK');
} catch (err) {
console.log(err);
}


מדפיס את ההודעה OK, אבל הקוד הזה מדפיס הודעת שגיאה כי הסיסמה קצרה מדי:

try {
await signupFormSchema.validate({email: 'ynon@demo.com', password: '123'});
console.log('OK');
} catch (err) {
console.log(err);
}


חיבור הסכימה לטופס
חיבור הסכימה לטופס הוא הסיבה שאני מעדיף כמעט תמיד להשתמש בפקודות המובנות ב HTML על פני קוד, במיוחד בעבודה עם ספריות SPA. אלה הדברים שהייתי צריך לעשות ב vue בשביל החיבור (וכן אני יודע שיש ספריות שיעשו את החיבור הזה בשבילי. להחביא קוד בתוך ספריה לא מעלים אותו):

1. הגדרתי משתנה ריאקטיבי שישמור את השגיאות.

2. הגדרתי משתנה ריאקטיבי נוסף שישמור את מצב ההגשה של הטופס (אם כבר הגשתי אותו או לא).

3. הגדרתי פונקציה לטפל באירוע submit של הטופס ולבצע וולידציה על המידע שבטופס.

4. הוספתי אלמנטי HTML להצגת הודעות השגיאה.

סך הכל קוד הטופס ב vue הוא:

<script setup lang="ts">
import {signupFormSchema} from '../schemas/signup1';
import { ValidationError } from 'yup';
import {computed, ref, Ref} from 'vue';
const errors = ref<Array<ValidationError>>([]);
const status = ref<'pending'|'submitted'>('pending');

const errorsObject: Ref<Record<string, Array<ValidationError>>> = computed(() =>
Object.groupBy(errors.value, e => e.path))

async function signup(ev: Event) {
const form = ev.target as HTMLFormElement;
const fd = new FormData(form);
const data = Object.fromEntries(fd);

try {
await signupFormSchema.validate(data, {abortEarly: false})
errors.value = [];
status.value = 'submitted';
} catch (err) {
if (err instanceof ValidationError) {
errors.value = err.inner;
console.log(err.inner);
} else {
throw err
}
}
}
</script>

<template>
<div v-if="status == 'submitted'">
Ready!
</div>
<form @submit.prevent="signup">
<div class="errors" v-if="errors.length > 0">
<h3>Form Erorrs:</h3>
<ul >
<li
v-for="(error, index) in errors"
:key="index"
>
{{ error.message }}
</li>
</ul>
</div>

<div>
<label>
<span>Email: </span>
<input name="email" type="email" />
</label>
</div>
<div>
<label>
<span>
Password:
</span>
<input name="password" type="password" />
</label>
</div>
<div>
<input type="submit" value="Signup" :disabled="status === 'submitted'" />
</div>
</form>
</template>

<style scoped>

label > span {
width: 100px;
display: inline-block;
}

form > div {
text-align: left;
margin: 10px;
}

.errors {
text-align: left;
list-style: none;
padding-left: 10px;
}
</style>


מסקנות
וולידציית קלטים בצד לקוח זה תמיד כאב ראש אבל זה רק נהיה יותר מעייף כשצריך לחבר בין הוולידציה לספריית SPA. ספריות קוד לטיפול אוטומטי בדברים האלה קיימות אבל האמת שבלי קוד תמיד עדיף על קוד שמוחבא בתוך ספרייה. שימוש בפקודות המובנות ב HTML חוסך אלמנטים עודפים ב DOM וחוסך משתנים ריאקטיביים וכך גם עבודת JavaScript.

ומה אתכם? איך אתם אוהבים לנהל את הטפסים שלכם? אל תתביישו לשתף בתגובות או בטלגרם.
איך כדאי לבנות
מתכנתים מנוסים יודעים יותר טוב לראות בעיות מראש ולחזות איפה דברים אולי יישברו. כשאנחנו מגייסים מתכנתים מנוסים נוכל להשתמש במיומנות זאת בראיונות עבודה וכך גם לקבל שיחה יותר מעניינת וגם לסנן טוב יותר אנשים שבאמת עבדו עם טכנולוגיה מסוימת ולמדו מטעויות שעשו. הנה כמה דוגמאות לסוג השאלות ששווה לחפש:

1. יש לי שרת REST API ואלה ה Endpoints שלו. מה יכולות להיות הבעיות בממשק שבחרתי? איזה דרישה או דרישות לקוח יהיה לי קשה לממש?

2. נתונה ספריית ריאקט וזה ה API שלה. באיזה סוגי מערכות יהיה קל לשלב אותה? ואיפה יהיה קשה?

3. נתון מבנה הטבלאות בבסיס הנתונים. למה כדאי לשים לב כשיוצרים את בסיס הנתונים? איזה פיצ'רים יהיה קל או קשה לממש עם טבלאות אלה? איזה פיצ'ר עלול לחייב אותי ל Data Migration משמעותי?

4. נתון קוד שעושה X. איזה דרישה חדשה יכולה להגיע מהלקוח שתשבור את הקוד הזה? עם איזה דרישות לקוח יהיה מאוד קל להתמודד בעזרת הקוד הקיים?

5. נתונים שלושה מימושים לפונקציה X, כולם מחזירים את אותה תוצאה. מה היתרונות והחסרונות של כל מימוש?

המשותף לכולן: במקום לשאול "איך עושים X" אנחנו עוברים לשאול "איך כדאי לעשות X".
"רק" עוד render אחד
הבעיה היא לא ה"רק" עוד render אחד שהקומפוננטה שלך מוסיפה למערכת בגלל שימוש לא נכון ב useEffect.

הבעיה היא מאות ה render-ים שיתווספו בארבע השנים הקרובות כשאנשים יעתיקו את הקוד הזה לעוד מקומות וישתמשו בקומפוננטה בדרכים לא צפויות.

יש רק שתי דרכים לזוז קדימה ולשמור על מהירות לאורך זמן:

1. אפשר לכתוב תמיד את הקוד הכי נכון (קשה מאוד).

2. אפשר לכתוב קוד עובד ופעם בכמה ימים לנקות אותו (גם קשה מאוד).

וכן ברור לי שאין דרך קלה לזוז קדימה במהירות לאורך זמן. החיים זה לא ספר לימוד וראינו כבר מספיק מערכות שיהיה קשה מדי להציל. ובכל זאת, נסו לזכור את זה בפעם הבאה שאתם חושבים "אף אחד לא ישים לב אם אוסיף רק עוד render אחד".
דוגמת קוד: וידוא קלט בשרת עם yup
לפני כמה ימים הראיתי כאן איך לבדוק קלט בטופס צד לקוח עם yup. ראינו ש yup מגיע עם המון יכולות אבל החיבור שלו לממשק דורש עבודה וברוב המקרים אפשר לקבל תוצאה טובה יותר דרך הכלים המובנים ב HTML. מצד שני בעבודת צד שרת אנחנו מגלים כמה yup יעיל וגם המבנה האסינכרוני שלו נראה מאוד הגיוני, כי ממילא כל ה APIs ב node הם אסינכרוניים. שימו לב לקוד הבא לצד שרת עבור REST API עם Hono:

app.post('/signup', async (c) => {
const body = await c.req.parseBody()
try {
await signupUserSchema.validate(body);
// create the user and redirect
return c.json({ ok: true });
} catch (err) {
throw new HTTPException(401, {message: String(err)})
}
});


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

ורק בשביל להשלים את התמונה זה קוד הסכימה מתוך הקובץ signupUserSchema:

import {object, string} from 'yup';

export default object().shape({
email: string().required().email(),
password: string().required().min(3),
});


וקוד הטופס:

<!DOCTYPE html>
<html>
<body>
<h1>Sign Up</h1>
<form action="/signup" method="post">
<label>
Email
<input type="email" name="email" required />
</label>

<label>
Password
<input type="password" name="password" required minlength="3" />
</label>

<input type="submit" />
</form>
</body>
</html>
הכלי הכי טוב
בשביל לכתוב יישום ריאקט גדול בלי באגים ולהצליח להתמודד עם דרישות חדשות שמגיעות לאורך זמן בטוח צריך להבין מה זה State Management, צריך להחליט אם רוצים להשתמש ב Context כדי לשמור סטייט, אם מסתפקים בספריית ניהול סטייט קטנה כמו Valtio או שהולכים על כל הקופה ומשלבים רידאקס. אבל בתחילת הדרך או כשהיישום קטן הכלים האלה יכולים מאוד לבלבל.

וכאן השאלה - עד כמה צריך להתאמץ להבין איך רידאקס עובד כשאנחנו רק לומדים ריאקט? עד כמה חשוב להבין איך GraphQL עובד כשאנחנו רק מתחילים ללמוד על פיתוח קוד צד שרת? עד כמה חשוב "להתרגל" לשיטת עבודה מסוימת רק בגלל שהיא Best Practice ותעזור לנו בעתיד?

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

הכלי הכי טוב הוא כלי שפותר בעיה שעכשיו יש לך.