لغة TypeScript

الأداء وتحسين الحزم

38 دقيقة الدرس 28 من 40

فهم حجم الحزمة والأداء

يتم محو TypeScript في وقت التشغيل، لكن اختيارات التكوين الخاصة بك تؤثر بشكل كبير على حجم الحزمة وفعالية إزالة الشفرة الميتة والأداء العام للتطبيق. يستكشف هذا الدرس تقنيات التحسين لبناءات الإنتاج.

الاستيرادات والصادرات للأنواع فقط

استيرادات الأنواع فقط تتم إزالتها أثناء التجميع ولا تنشئ تبعيات وقت التشغيل:

// ❌ سيء: ينشئ استيراد وقت التشغيل import { User } from './types'; function getUser(): User { return { id: 1, name: 'John' }; } // ✓ جيد: استيراد نوع فقط (يتم إزالته في وقت التشغيل) import type { User } from './types'; function getUser(): User { return { id: 1, name: 'John' }; } // ✓ جيد أيضاً: استيراد نوع مضمن import { type User, fetchUser } from './api';

صادرات الأنواع فقط لإعادة التصدير:

// types.ts export interface User { id: number; name: string; } export interface Product { id: number; title: string; } // index.ts - ❌ سيء: ينشئ تبعية وقت التشغيل export { User, Product } from './types'; // index.ts - ✓ جيد: تصدير نوع فقط export type { User, Product } from './types';
ملاحظة: استخدم import type و export type لجعل الاستيرادات للأنواع فقط صريحة. هذا يساعد أدوات التجميع على إزالة الشفرة الميتة بشكل أكثر فعالية ويوضح نيتك.

علامة isolatedModules

قم بتمكين isolatedModules لتوافق أفضل مع أدوات التحويل أحادية الملف:

{ "compilerOptions": { "isolatedModules": true } }

هذه العلامة تفرض قيوداً تمنع الأنماط التي لا يمكن تفسيرها بشكل صحيح بواسطة أدوات التحويل أحادية الملف مثل Babel أو esbuild أو swc:

// ❌ خطأ مع isolatedModules: إعادة تصدير نوع يحتاج كلمة type صريحة export { User } from './types'; // ✓ صحيح export type { User } from './types'; export { type User } from './types'; // صحيح أيضاً // ❌ خطأ: const enums غير مدعومة const enum Direction { Up, Down } // ✓ صحيح: enum عادي enum Direction { Up, Down } // ❌ خطأ: لا يمكن تصدير إعلان نوع فقط export interface User { } // ✓ صحيح: تصدير مع التنفيذ export interface User { } export const createUser = () => ({ id: 1, name: 'test' });

Const Enums وإزالة الشفرة الميتة

const enums يتم تضمينها مباشرة أثناء التجميع:

// enum عادي (ينشئ كائن وقت التشغيل) enum Direction { Up = 'UP', Down = 'DOWN', Left = 'LEFT', Right = 'RIGHT' } const move = Direction.Up; // الإخراج المترجم يتضمن كائن enum: var Direction; (function (Direction) { Direction["Up"] = "UP"; Direction["Down"] = "DOWN"; Direction["Left"] = "LEFT"; Direction["Right"] = "RIGHT"; })(Direction || (Direction = {})); const move = Direction.Up;
// Const enum (مضمن في وقت التجميع) const enum Direction { Up = 'UP', Down = 'DOWN', Left = 'LEFT', Right = 'RIGHT' } const move = Direction.Up; // الإخراج المترجم مضمن: const move = "UP"; // لا يوجد كائن enum في وقت التشغيل!
تحذير: Const enums غير متوافقة مع isolatedModules لأن أدوات التحويل أحادية الملف لا يمكنها تضمين القيم عبر الملفات. استخدم enums عادية أو أنواع الاتحاد بدلاً من ذلك إذا كنت تستخدم أدوات تجميع حديثة.

أنواع الاتحاد مقابل Enums لأداء أفضل

أنواع الاتحاد ليس لها تكلفة في وقت التشغيل:

// ❌ تكلفة وقت التشغيل enum Status { Pending = 'PENDING', Approved = 'APPROVED', Rejected = 'REJECTED' } // ✓ تكلفة صفرية في وقت التشغيل type Status = 'PENDING' | 'APPROVED' | 'REJECTED'; // استخدم مع كائن const للوصول إلى القيمة const STATUS = { Pending: 'PENDING', Approved: 'APPROVED', Rejected: 'REJECTED' } as const; type Status = typeof STATUS[keyof typeof STATUS]; function updateStatus(status: Status): void { console.log(status); } updateStatus(STATUS.Pending); // آمن للأنواع مع الإكمال التلقائي

تكوين إزالة الشفرة الميتة

تحسين لإزالة الشفرة الميتة في tsconfig.json:

{ "compilerOptions": { // استخدم وحدات ES لأفضل إزالة للشفرة الميتة "module": "ESNext", "target": "ES2020", // تمكين للتوافق مع Babel/esbuild/swc "isolatedModules": true, // احتفظ ببيانات import/export "esModuleInterop": true, "allowSyntheticDefaultImports": true, // لا تحافظ على const enums (استخدم enums/اتحادات عادية) "preserveConstEnums": false, // إزالة التعليقات لتقليل الحجم "removeComments": true, // لا تصدر بيانات تعريف decorator إلا إذا لزم الأمر "emitDecoratorMetadata": false } }

الآثار الجانبية و package.json

وضع علامة على الوحدات كخالية من الآثار الجانبية لإزالة عدوانية للشفرة الميتة:

// package.json { "name": "my-library", "sideEffects": false, // لا توجد ملفات لها آثار جانبية // أو حدد الملفات التي لها آثار جانبية "sideEffects": [ "*.css", "*.scss", "./src/polyfills.ts" ] }

اكتب شفرة خالية من الآثار الجانبية:

// ❌ له تأثير جانبي (تنفيذ على مستوى الوحدة) const API_KEY = process.env.API_KEY; console.log('Module loaded!'); export function makeRequest() { return fetch(`/api?key=${API_KEY}`); } // ✓ خالٍ من الآثار الجانبية export function makeRequest() { const API_KEY = process.env.API_KEY; return fetch(`/api?key=${API_KEY}`); }

التحميل الكسول للأنواع

استخدم الاستيرادات الديناميكية لتقسيم الشفرة:

// مكتبة ثقيلة ليست مطلوبة دائماً import type { Chart } from 'chart.js'; // التحميل الكسول فقط عند الحاجة async function renderChart(data: number[]): Promise<void> { const { Chart } = await import('chart.js'); const chart = new Chart(/* ... */); // استخدم chart... } // أو مع React.lazy const ChartComponent = React.lazy(() => import('./ChartComponent'));

تحليل تكلفة الاستيراد

تحليل ما تستورده فعلياً:

// ❌ سيء: يستورد المكتبة بأكملها (حتى لو كانت قابلة لإزالة الشفرة الميتة) import _ from 'lodash'; const result = _.debounce(fn, 300); // ✓ أفضل: استيراد مسمى (قابل لإزالة الشفرة الميتة) import { debounce } from 'lodash'; const result = debounce(fn, 300); // ✓ الأفضل: استيراد مسار مباشر (أصغر حزمة) import debounce from 'lodash/debounce'; const result = debounce(fn, 300); // لـ lodash-es الحديث (قابل لإزالة الشفرة الميتة افتراضياً) import { debounce } from 'lodash-es';

ملفات الإعلان والأداء

تحسين إعلانات الأنواع لنشر المكتبات:

{ "compilerOptions": { // إنشاء الإعلانات "declaration": true, "declarationMap": true, // فحص أسرع للأنواع للمستهلكين "skipLibCheck": true, // ملف .d.ts واحد (حل أسرع) "declarationDir": "./dist/types", // إزالة الأنواع الداخلية "stripInternal": true } }

استخدم تعليق JSDoc @internal لإخفاء تفاصيل التنفيذ:

/** * دالة API عامة */ export function publicApi(): void { internalHelper(); } /** * @internal * تفاصيل تنفيذ داخلية (تمت إزالتها من الإعلانات) */ export function internalHelper(): void { // التنفيذ }

التجميع المتزايد

تسريع بناءات التطوير مع التجميع المتزايد:

{ "compilerOptions": { "incremental": true, "tsBuildInfoFile": "./.tsbuildinfo", // للمستودعات الأحادية "composite": true } }

ملف معلومات البناء يخزن معلومات الأنواع مؤقتاً بين البناءات:

# البناء الأول: أبطأ tsc --build # البناءات اللاحقة: أسرع بكثير (الملفات المتغيرة فقط) tsc --build # تنظيف معلومات البناء tsc --build --clean

أداء حل الوحدات

تحسين حل الوحدات لتجميع أسرع:

{ "compilerOptions": { // حل أسرع مع bundler "moduleResolution": "bundler", // تخطي فحص المكتبة (تسريع هائل) "skipLibCheck": true, // حدد جذور الأنواع لتقليل البحث "typeRoots": ["./node_modules/@types"], // قصر الأنواع على ما تحتاجه فقط "types": ["node", "jest"], // عنوان URL الأساسي لتعيين المسار "baseUrl": ".", "paths": { "@/*": ["src/*"] } } }

أنماط أداء وقت التشغيل

اكتب TypeScript يترجم إلى JavaScript أداء:

// ❌ بطيء: ينشئ كائن جديد في كل استدعاء function getConfig() { return { apiUrl: 'https://api.example.com', timeout: 5000, retries: 3 }; } // ✓ سريع: يعيد استخدام نفس الكائن const CONFIG = { apiUrl: 'https://api.example.com', timeout: 5000, retries: 3 } as const; function getConfig() { return CONFIG; } // ❌ بطيء: حارس النوع بفحوصات معقدة function isUser(obj: unknown): obj is User { return ( typeof obj === 'object' && obj !== null && 'id' in obj && typeof obj.id === 'number' && 'name' in obj && typeof obj.name === 'string' && 'email' in obj && typeof obj.email === 'string' ); } // ✓ سريع: فحص بنيوي مع عوائد مبكرة function isUser(obj: unknown): obj is User { if (typeof obj !== 'object' || obj === null) return false; const u = obj as User; return typeof u.id === 'number' && typeof u.name === 'string'; }

نصائح تحسين Webpack/Vite

قم بتكوين أداة التجميع الخاصة بك لبناءات TypeScript المثلى:

تكوين Vite:

// vite.config.ts import { defineConfig } from 'vite'; export default defineConfig({ build: { // تمكين التصغير minify: 'esbuild', // تمكين إزالة الشفرة الميتة target: 'es2020', // تقسيم القطع rollupOptions: { output: { manualChunks: { 'vendor': ['react', 'react-dom'], 'utils': ['./src/utils/index'] } } } }, // استخدم esbuild لتحويل أسرع esbuild: { target: 'es2020', drop: ['console', 'debugger'] // إزالة في الإنتاج } });

تكوين Webpack:

// webpack.config.js module.exports = { module: { rules: [ { test: /\.tsx?$/, use: 'esbuild-loader', // أسرع بكثير من ts-loader options: { loader: 'tsx', target: 'es2020' } } ] }, optimization: { usedExports: true, // إزالة الشفرة الميتة sideEffects: true, // احترام حقل sideEffects splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: 10 } } } } };

قياس حجم الحزمة

أدوات لتحليل تكوين الحزمة:

# تثبيت محلل الحزمة npm install --save-dev webpack-bundle-analyzer # تحليل حزمة Vite npm install --save-dev rollup-plugin-visualizer # فحص حد الحجم npm install --save-dev size-limit @size-limit/preset-app
// package.json { "scripts": { "analyze": "vite build --mode analyze", "size": "size-limit" }, "size-limit": [ { "path": "dist/index.js", "limit": "50 KB" } ] }

قائمة التحقق من بناء الإنتاج

قائمة التحقق من التحسين:
  • ✓ استخدم import type للاستيرادات للأنواع فقط
  • ✓ قم بتمكين isolatedModules لتحويل أسرع
  • ✓ فضل أنواع الاتحاد على enums عندما يكون ذلك ممكناً
  • ✓ اضبط skipLibCheck: true لبناءات أسرع
  • ✓ استخدم الاستيرادات المسماة من المكتبات القابلة لإزالة الشفرة الميتة
  • ✓ وضع علامة على الحزم كخالية من الآثار الجانبية في package.json
  • ✓ قم بتمكين removeComments في الإنتاج
  • ✓ استخدم تقسيم الشفرة للتبعيات الكبيرة
  • ✓ قم بتحليل حجم الحزمة بانتظام
  • ✓ قم بإعداد حدود حجم الحزمة في CI

مثال من العالم الحقيقي: قبل وبعد

// ❌ قبل التحسين (أداء حزمة ضعيف) import _ from 'lodash'; import moment from 'moment'; import * as utils from './utils'; enum ApiStatus { Loading = 0, Success = 1, Error = 2 } const config = { apiUrl: process.env.API_URL }; export class ApiService { constructor() { console.log('ApiService initialized'); } formatDate(date: Date): string { return moment(date).format('YYYY-MM-DD'); } debounceRequest = _.debounce(this.makeRequest, 300); makeRequest(data: any): void { // التنفيذ } } // ✓ بعد التحسين import type { RequestData } from './types'; import debounce from 'lodash/debounce'; type ApiStatus = 'loading' | 'success' | 'error'; const getConfig = () => ({ apiUrl: process.env.API_URL }); export class ApiService { formatDate(date: Date): string { return date.toISOString().split('T')[0]; } debounceRequest = debounce(this.makeRequest.bind(this), 300); makeRequest(data: RequestData): void { // التنفيذ } } // توفير الحزمة: ~200 KB → ~15 KB
تمرين:
  1. قم بإعداد تحليل الحزمة لمشروعك باستخدام webpack-bundle-analyzer أو rollup-plugin-visualizer
  2. حدد أكبر 3 تبعيات في حزمتك
  3. استبدل أي استيرادات افتراضية باستيرادات مسماة حيثما أمكن
  4. قم بتحويل enums إلى أنواع اتحاد وكائنات const
  5. أضف import type لجميع الاستيرادات للأنواع فقط
  6. قس حجم الحزمة قبل وبعد التحسينات
  7. قم بإعداد size-limit لمنع تراجع حجم الحزمة