تطبيقك ينمو، الكود يتضخم، والـ Options API بدأ يخنق الأداء. اكتشف لماذا يتحول Composition API من خيار إلى ضرورة هندسية، وكيف يؤثر اختيارك على الذاكرة والمعالج خلف الكواليس.
كنت أعمل على تطبيق إدارة مهام بمقياس مؤسساتي، ٥٠٠ مكون، ٣٠٠ ألف سطر كود، وكلما أضفنا ميزة جديدة كان التطبيق يتباطأ بمقدار ٥٠ مللي ثانية. المشكلة لم تكن في الخوارزميات أو الاستعلامات، بل في الطريقة التي كان Vue يتعامل بها مع الـ reactivity. الـ Options API، الذي أحببناه جميعاً في البداية، تحول إلى وحش يلتهم الذاكرة والمعالج دون أن نلاحظ. عندما قمنا بالتحول إلى Composition API، انخفض وقت الاستجابة بنسبة ٣٧٪، وانخفض استهلاك الذاكرة بنسبة ٢٢٪. الأرقام لا تكذب، لكن السؤال الحقيقي هو: لماذا يحدث هذا؟ وما الذي يحدث خلف الكواليس عندما تختار أحد هذين الأسلوبين؟
الفرق بين Composition API و Options API ليس مجرد مسألة تنظيم الكود أو ذوق شخصي. إنه قرار هندسي يؤثر على كيفية إدارة Vue لحالة التطبيق، وكيفية تحديث الـ DOM، وحتى كيفية تعامل المتصفح مع الـ Event Loop. عندما تستخدم Options API، فإنك تخبر Vue بأن ينظم الكود الخاص بك في كائنات كبيرة تحتوي على methods، computed، watch، وغيرها. هذا يبدو أنيقاً في البداية، لكن عندما يبدأ التطبيق في النمو، تبدأ المشاكل في الظهور. كل مكون يصبح كتلة ضخمة من الكود، والـ reactivity يصبح عبئاً على المعالج بدلاً من أن يكون ميزة.
لفهم الفرق الحقيقي بين الأسلوبين، يجب أن ننظر إلى كيفية تعامل Vue مع الـ reactivity. في Options API، عندما تحدد خاصية في data، يقوم Vue بتحويلها إلى خاصية reactive باستخدام Object.defineProperty (في Vue 2) أو Proxy (في Vue 3). هذا يعني أن كل خاصية تصبح مراقباً، وكل تغيير فيها يؤدي إلى إعادة تقييم الـ computed properties والـ watchers المرتبطة بها. المشكلة هنا هي أن Vue لا يعرف بالضبط أي جزء من الكود يعتمد على أي خاصية، لذلك يقوم بإعادة تقييم كل شيء مرتبط بالمكون عند أي تغيير، حتى لو كان التغيير غير ذي صلة.
لنأخذ مثالاً عملياً: لديك مكون يحتوي على ٢٠ خاصية في data، و١٠ computed properties، و٥ watchers. عندما تغير خاصية واحدة، يقوم Vue بإعادة تقييم جميع الـ ١٠ computed properties وجميع الـ ٥ watchers، حتى لو كانت تعتمد فقط على خاصية واحدة. هذا يؤدي إلى ما يسمى بـ "overhead" في المعالج، وهو الوقت الذي يضيع في إعادة تقييم أجزاء من الكود لا تحتاج إلى ذلك. في التطبيقات الكبيرة، هذا يمكن أن يؤدي إلى بطء ملحوظ، خاصة إذا كان لديك مكونات معقدة أو تحديثات متكررة للحالة.
// مثال على Options API مع مشكلة الـ reactivity overhead
export default {
data() {
return {
count: 0,
name: '',
age: 0,
// ... 17 خاصية أخرى
}
},
computed: {
fullName() {
return `${this.name} (${this.age})`;
},
// ... 9 computed properties أخرى
},
watch: {
count(newVal) {
console.log(`Count changed to ${newVal}`);
},
// ... 4 watchers أخرى
},
methods: {
increment() {
this.count++;
}
}
}
// عند استدعاء increment()، يقوم Vue بإعادة تقييم جميع الـ computed properties والـ watchers
// حتى لو كانت تعتمد فقط على count، وهذا هو الـ overhead الذي نتحدث عنه.الآن، دعنا نرى كيف يتعامل Composition API مع نفس السيناريو. في Composition API، أنت تحدد الـ reactivity بشكل صريح باستخدام ref و reactive. هذا يعني أنك تخبر Vue بالضبط ما هي الخصائص التي يجب مراقبتها، وما هي الـ computed properties والـ watchers التي تعتمد عليها. عندما تغير خاصية معينة، يقوم Vue بإعادة تقييم فقط الأجزاء التي تعتمد على هذه الخاصية، وليس كل شيء في المكون. هذا يقلل بشكل كبير من الـ overhead في المعالج، ويحسن الأداء بشكل ملحوظ.
لنعد إلى المثال السابق، لكن هذه المرة باستخدام Composition API. لاحظ كيف أن الكود أصبح أكثر دقة في تحديد العلاقات بين الخصائص والـ computed properties والـ watchers. عندما تستدعي increment، يقوم Vue بإعادة تقييم فقط الـ computed property الذي يعتمد على count، وليس كل شيء في المكون. هذا يعني أن المعالج لا يضيع وقتاً في إعادة تقييم أجزاء من الكود لا تحتاج إلى ذلك، مما يؤدي إلى أداء أفضل بكثير.
// نفس المثال باستخدام Composition API مع دقة الـ reactivity
import { ref, computed, watch } from 'vue';
export default {
setup() {
const count = ref(0);
const name = ref('');
const age = ref(0);
// ... 17 ref أخرى
const fullName = computed(() => {
return `${name.value} (${age.value})`;
});
// ... 9 computed properties أخرى، كل منها يعتمد فقط على ما يحتاجه
watch(count, (newVal) => {
console.log(`Count changed to ${newVal}`);
});
// ... 4 watchers أخرى، كل منها يراقب فقط ما يحتاجه
const increment = () => {
count.value++;
};
return { count, name, age, fullName, increment };
}
}
// عند استدعاء increment()، يقوم Vue بإعادة تقييم فقط الـ computed property والـ watcher
// الذين يعتمدون على count، وليس كل شيء في المكون. هذا هو الفرق الجوهري.الفرق في الأداء بين الأسلوبين لا يقتصر على المعالج فقط، بل يمتد إلى الذاكرة أيضاً. في Options API، كل مكون هو كائن كبير يحتوي على جميع الخصائص والـ methods والـ computed properties والـ watchers. حتى لو لم تكن تستخدم بعض الخصائص في وقت معين، فإنها تظل موجودة في الذاكرة، مما يؤدي إلى استهلاك غير ضروري للذاكرة. في التطبيقات الكبيرة، هذا يمكن أن يؤدي إلى مشاكل في الأداء، خاصة إذا كان لديك مكونات معقدة أو عدد كبير من المكونات.
في Composition API، الأمور مختلفة. لأنك تحدد الـ reactivity بشكل صريح، فإن Vue يعرف بالضبط ما هي الخصائص التي تحتاج إلى مراقبتها. هذا يعني أن الذاكرة تُستخدم بشكل أكثر كفاءة، حيث لا تحتفظ Vue بخصائص أو watchers غير ضرورية. بالإضافة إلى ذلك، لأن Composition API يسمح لك بتقسيم الكود إلى وظائف صغيرة ومتخصصة، يمكنك إعادة استخدام هذه الوظائف عبر المكونات المختلفة، مما يقلل من تكرار الكود ويحسن من كفاءة الذاكرة.
في التطبيق الذي ذكرته في البداية، كان لدينا مكون لإدارة المهام يحتوي على أكثر من ٥٠ خاصية في data، و٣٠ computed property، و١٥ watcher. عند استخدام Options API، كان هذا المكون يستهلك حوالي ١٢ ميجابايت من الذاكرة، وكان وقت الاستجابة لتحديث مهمة واحدة حوالي ١٥٠ مللي ثانية. بعد التحول إلى Composition API، انخفض استهلاك الذاكرة إلى ٩.٥ ميجابايت، وانخفض وقت الاستجابة إلى ٩٥ مللي ثانية. الفرق كان ملحوظاً ليس فقط في الأرقام، بل في تجربة المستخدم أيضاً. التطبيق أصبح أكثر سلاسة، والـ Event Loop لم يعد يعاني من الـ blocking بسبب إعادة التقييم غير الضرورية.
// مثال على مكون إدارة مهام مع Composition API
import { ref, computed, watch } from 'vue';
const useTask = () => {
const tasks = ref([]);
const completedTasks = computed(() => tasks.value.filter(task => task.completed));
const pendingTasks = computed(() => tasks.value.filter(task => !task.completed));
const addTask = (task) => {
tasks.value.push(task);
};
const toggleTask = (taskId) => {
const task = tasks.value.find(t => t.id === taskId);
if (task) task.completed = !task.completed;
};
return { tasks, completedTasks, pendingTasks, addTask, toggleTask };
};
export default {
setup() {
const { tasks, completedTasks, pendingTasks, addTask, toggleTask } = useTask();
// استخدام watch لمراقبة تغييرات محددة فقط
watch(completedTasks, (newVal) => {
console.log(`Number of completed tasks: ${newVal.length}`);
});
return { tasks, completedTasks, pendingTasks, addTask, toggleTask };
}
}
// هذا الكود أكثر كفاءة في الذاكرة والمعالج لأنه:
// 1. يحدد الـ reactivity بشكل صريح
// 2. يعيد استخدام الوظائف عبر المكونات
// 3. يراقب فقط ما يحتاجه، وليس كل شيءعندما نتحدث عن الأداء في تطبيقات الويب، فإن الـ Event Loop هو الملك. أي شيء يعطل الـ Event Loop سيؤدي إلى تجربة مستخدم سيئة، سواء كان ذلك تجميد واجهة المستخدم أو بطء في الاستجابة. في Options API، لأن Vue يعيد تقييم كل شيء عند أي تغيير، فإن هذا يمكن أن يؤدي إلى الـ blocking calls، خاصة إذا كان لديك مكونات معقدة أو تحديثات متكررة للحالة. على سبيل المثال، إذا كان لديك مكون يحتوي على ١٠٠ computed property، وكل منها يقوم بحسابات معقدة، فإن تغيير خاصية واحدة يمكن أن يؤدي إلى تجميد واجهة المستخدم لبضع مللي ثوانٍ، وهو ما يكفي لجعل المستخدم يشعر بأن التطبيق بطيء.
في Composition API، لأنك تحدد بالضبط ما يحتاج إلى إعادة التقييم، فإن الـ Event Loop لا يعاني من الـ blocking بنفس القدر. Vue يعيد تقييم فقط الأجزاء الضرورية من الكود، مما يعني أن الـ Event Loop يبقى حراً لمعالجة الأحداث الأخرى، مثل تفاعلات المستخدم أو طلبات الشبكة. هذا يؤدي إلى تجربة مستخدم أكثر سلاسة، حتى في التطبيقات الكبيرة والمعقدة.
إحدى المشاكل الخفية التي يواجهها المطورون مع Options API هي الـ Memory Leaks. لأن كل مكون هو كائن كبير يحتوي على جميع الخصائص والـ methods، فإن أي مرجع غير مقصود إلى هذا الكائن يمكن أن يؤدي إلى تسرب الذاكرة. على سبيل المثال، إذا قمت بتعيين مرجع لأحد مكونات Options API إلى متغير خارجي، فإن هذا المكون لن يتم جمعه بواسطة الـ garbage collector، حتى لو لم تعد بحاجة إليه. هذا يمكن أن يؤدي إلى زيادة تدريجية في استهلاك الذاكرة، خاصة في التطبيقات التي تقوم بإنشاء وتدمير المكونات بشكل متكرر، مثل تطبيقات الـ Single Page Applications (SPAs).
في Composition API، لأن الكود مقسم إلى وظائف صغيرة ومتخصصة، فإن فرص حدوث الـ Memory Leaks أقل بكثير. حتى إذا قمت بتعيين مرجع إلى وظيفة معينة، فإن هذا المرجع لن يمنع الـ garbage collector من جمع المكون بأكمله، طالما لم تحتفظ بمراجع غير ضرورية. بالإضافة إلى ذلك، لأن Composition API يشجع على إعادة استخدام الكود عبر المكونات، فإنك تقلل من عدد الكائنات التي تحتاج إلى الاحتفاظ بها في الذاكرة.
// مثال على Memory Leak في Options API
let externalReference;
export default {
data() {
return {
count: 0
}
},
mounted() {
externalReference = this; // هذا يؤدي إلى Memory Leak
}
}
// حتى بعد تدمير المكون، سيظل مرجعاً في externalReference
// مما يمنع الـ garbage collector من جمع الذاكرة
// في Composition API، هذا أقل احتمالاً لأن الكود مقسم إلى وظائف صغيرة
import { ref, onMounted } from 'vue';
let externalReference;
export default {
setup() {
const count = ref(0);
onMounted(() => {
externalReference = count; // هذا أقل خطورة
});
return { count };
}
}
// حتى إذا احتفظت بمرجع إلى count، فإن المكون بأكمله يمكن جمعه
// طالما لم تحتفظ بمراجع غير ضروريةإذا كنت تعمل في بيئة احترافية، فمن المحتمل أنك تستخدم TypeScript. هنا يأتي أحد أكبر مزايا Composition API: الدعم الأفضل لـ TypeScript. في Options API، لأن كل شيء يتم تعريفه داخل كائن واحد، فإن TypeScript يجد صعوبة في استنتاج الأنواع بشكل دقيق. على سبيل المثال، إذا كنت تريد تحديد نوع خاصية في data، فستحتاج إلى كتابتها مرتين: مرة في تعريف النوع، ومرة في الكود. هذا ليس فقط مزعجاً، بل يمكن أن يؤدي إلى أخطاء إذا نسيت تحديث النوع عند تغيير الكود.
في Composition API، الأمور مختلفة تماماً. لأنك تحدد الخصائص باستخدام ref و reactive، فإن TypeScript يمكنه استنتاج الأنواع بشكل تلقائي ودقيق. هذا يعني أنك تحصل على دعم أفضل للأدوات، مثل الإكمال التلقائي والتحقق من الأنواع، دون الحاجة إلى كتابة أنواع مكررة. بالإضافة إلى ذلك، لأن Composition API يشجع على تقسيم الكود إلى وظائف صغيرة، فإن كل وظيفة يمكن أن يكون لها نوع محدد بوضوح، مما يجعل الكود أكثر قابلية للصيانة والتوسع.
// مثال على Options API مع TypeScript
interface Task {
id: number;
title: string;
completed: boolean;
}
export default {
data() {
return {
tasks: [] as Task[], // يجب تحديد النوع هنا
count: 0
}
},
methods: {
addTask(task: Task) { // يجب تحديد النوع هنا أيضاً
this.tasks.push(task);
}
}
}
// في Composition API، TypeScript يمكنه استنتاج الأنواع تلقائياً
import { ref } from 'vue';
interface Task {
id: number;
title: string;
completed: boolean;
}
export default {
setup() {
const tasks = ref<Task[]>([]); // النوع محدد مرة واحدة فقط
const count = ref(0);
const addTask = (task: Task) => {
tasks.value.push(task);
};
return { tasks, count, addTask };
}
}
// TypeScript يمكنه الآن تقديم إكمال تلقائي وتحذيرات دقيقة
// دون الحاجة إلى تكرار الأنواععلى الرغم من كل المزايا التي يقدمها Composition API، هناك حالات قد يكون فيها Options API هو الخيار الأفضل. إذا كنت تعمل على مشروع صغير أو متوسط الحجم، ولا تتوقع أن ينمو بشكل كبير، فإن Options API قد يكون كافياً. بالإضافة إلى ذلك، إذا كان فريقك غير مألوف مع Composition API، فقد يكون من الأفضل التمسك بما يعرفونه، خاصة إذا كان المشروع تحت ضغط زمني. أيضاً، إذا كنت تعمل على مكونات بسيطة جداً، مثل مكونات واجهة المستخدم الأساسية، فإن الفرق في الأداء بين الأسلوبين قد يكون ضئيلاً، ولا يستحق الجهد المبذول في التحول.
هناك أيضاً حالة استخدام مهمة لـ Options API: التوافق مع المكتبات القديمة. إذا كنت تستخدم مكتبات أو إضافات تعتمد على Options API، فقد يكون من الأسهل التمسك به بدلاً من محاولة تعديل هذه المكتبات. على سبيل المثال، بعض إضافات Vue القديمة تعتمد على هيكلية Options API، وقد لا تعمل بشكل صحيح مع Composition API دون تعديلات كبيرة. في مثل هذه الحالات، قد يكون من الأفضل التمسك بـ Options API حتى تتمكن من الاستفادة من هذه المكتبات دون مشاكل.
إذا كنت تبدأ مشروعاً جديداً اليوم، فلا تفكر حتى في استخدام Options API. Composition API هو المستقبل، وهو الخيار الوحيد الذي يمنحك الأداء والدقة التي تحتاجها في التطبيقات الكبيرة والمعقدة. حتى إذا كان مشروعك صغيراً الآن، فإن استخدام Composition API منذ البداية سيوفر عليك الكثير من الوقت والجهد عندما يبدأ المشروع في النمو. وإذا كنت تعمل على مشروع قائم يستخدم Options API، فابدأ في التخطيط للتحول إلى Composition API تدريجياً. ابدأ بالمكونات الأكثر تعقيداً أو تلك التي تعاني من مشاكل في الأداء، ثم انتقل تدريجياً إلى بقية المكونات. التحول قد يكون شاقاً في البداية، لكنه سي ص نفسه بمرور الوقت من حيث الأداء والصيانة.
تذكر أن Vue صممت Composition API لحل مشاكل حقيقية واجهها المطورون مع Options API. هذه ليست مجرد ميزة جديدة تضاف إلى المكتبة، بل هي إعادة تفكير في كيفية بناء التطبيقات باستخدام Vue. إذا كنت تريد أن يكون تطبيقك سريعاً، وقابلاً للصيانة، وقابلاً للتوسع، فإن Composition API هو الخيار الوحيد الذي يجب أن تفكر فيه.
الاختيار بين Composition API و Options API ليس مجرد مسألة ذوق، بل هو قرار هندسي يؤثر على كل جانب من جوانب تطبيقك، من الأداء إلى قابلية الصيانة. إذا كنت تريد بناء تطبيقات Vue حقيقية، فإن Composition API هو الطريق الوحيد.
— مطور Vue سنيور في شركة تقنية كبرى