React Profiler ба оновчлол
React апп удаан ажиллаж байна гэж мэдэрвэл хамгийн түрүүнд хаана удааширч байгааг олох хэрэгтэй. Таамаглалаар оновчлол хийх нь цаг хугацаа алдаж, хэрэггүй нарийн төвөгтэй код гаргадаг. React DevTools Profiler болон кодын оновчлолын хэрэгслүүдийг зөв хослуулбал гүйцэтгэлийн асуудлыг системтэй шийдэж болно.
React DevTools Profiler
React DevTools-ыг браузерт суулгасны дараа Profiler tab-аас ажиллуулна:
- Chrome-д React DevTools extension суулгах
- DevTools нээгээд Profiler tab сонгох
- Дугуй бичлэгийн товч дарж аппыг ашиглах
- Бичлэг зогсоож үр дүн харах
Profiler нь "flame graph" харуулдаг — өргөн мөр нь удаан render хийсэн component, өнгө нь өөрчлөгдсөн эсэхийг илэрхийлнэ.
Кодод <Profiler> component ашиглан хэмжилтийг программчлан хийж болно:
import { Profiler, ProfilerOnRenderCallback } from "react";
const onRender: ProfilerOnRenderCallback = (
id, // Profiler-н нэр
phase, // "mount" эсвэл "update"
actualDuration, // render хийхэд зарцуулсан ms
baseDuration, // оновчлолгүй render-н tailan ms
) => {
if (actualDuration > 16) {
// 16ms = 60fps-д нэг frame — үүнээс удаан бол анхаарах хэрэгтэй
console.warn(`${id} удаан render: ${actualDuration.toFixed(2)}ms`);
}
};
function App() {
return (
<Profiler id="CourseList" onRender={onRender}>
<CourseList />
</Profiler>
);
}
React.memo — хэтэрхий олон render зогсоох
Parent component render хийх бүрт child component-ууд ч дахин render хийдэг — props нь өөрчлөгдөөгүй байсан ч. React.memo нь props өөрчлөгдөөгүй бол render хийхгүй байх боломж олгодог:
// React.memo-гүй — parent render хийх бүрт дахин render болно
function LessonItem({ lesson, isCompleted }: LessonItemProps) {
console.log("LessonItem render:", lesson.title);
return <li className={isCompleted ? "done" : ""}>{lesson.title}</li>;
}
// React.memo-тэй — props өөрчлөгдсөн үед л render болно
const LessonItem = React.memo(function LessonItem({
lesson,
isCompleted,
}: LessonItemProps) {
return <li className={isCompleted ? "done" : ""}>{lesson.title}</li>;
});
React.memo хэзээ ашиглах вэ?
// ✅ Тохиромжтой нөхцөл:
// — Жагсаалтад олон удаа гарч ирдэг component
// — Render нь үнэтэй (тооцоолол ихтэй) component
// — Props нь ховор өөрчлөгддөг component
// ❌ Хэт ашиглах шаардлагагүй нөхцөл:
// — Props байнга өөрчлөгддөг component
// — Маш энгийн, хурдан render хийдэг component
// — Context-оос уншдаг component (Context өөрчлөгдвөл memo тус болохгүй)
useMemo — үнэтэй тооцооллыг кэшлэх
Render бүрт дахин тооцохын хэрэггүй үнэтэй тооцооллыг useMemo хадгалдаг:
import { useMemo, useState } from "react";
interface Lesson {
id: number;
title: string;
isCompleted: boolean;
duration: number;
}
function CourseDashboard({ lessons }: { lessons: Lesson[] }) {
const [filter, setFilter] = useState<"all" | "done" | "pending">("all");
// ❌ Render бүрт дахин тооцдог
const stats = {
completed: lessons.filter((l) => l.isCompleted).length,
totalMinutes: lessons.reduce((sum, l) => sum + l.duration, 0),
};
// ✅ lessons өөрчлөгдсөн үед л дахин тооцдог
const memoStats = useMemo(
() => ({
completed: lessons.filter((l) => l.isCompleted).length,
totalMinutes: lessons.reduce((sum, l) => sum + l.duration, 0),
}),
[lessons],
);
// ✅ lessons болон filter аль нэг өөрчлөгдсөн үед л шүүнэ
const filteredLessons = useMemo(() => {
if (filter === "done") return lessons.filter((l) => l.isCompleted);
if (filter === "pending") return lessons.filter((l) => !l.isCompleted);
return lessons;
}, [lessons, filter]);
return (
<div>
<p>
Дууссан: {memoStats.completed}/{lessons.length}
</p>
<p>Нийт: {memoStats.totalMinutes} мин</p>
<select
value={filter}
onChange={(e) => setFilter(e.target.value as typeof filter)}
>
<option value="all">Бүгд</option>
<option value="done">Дууссан</option>
<option value="pending">Хүлээгдэж буй</option>
</select>
<ul>
{filteredLessons.map((lesson) => (
<LessonItem key={lesson.id} lesson={lesson} />
))}
</ul>
</div>
);
}
useCallback — функц reference тогтмол байлгах
React.memo ашигласан child component-д функц prop дамжуулахад useCallback шаардлагатай болдог — учир нь render бүрт шинэ функц үүсэж, memo-г тойрч гардаг:
import { useCallback, useState, memo } from "react";
const LessonRow = memo(function LessonRow({
lesson,
onComplete,
onDelete,
}: {
lesson: Lesson;
onComplete: (id: number) => void;
onDelete: (id: number) => void;
}) {
console.log("LessonRow render:", lesson.title);
return (
<tr>
<td>{lesson.title}</td>
<td>
<button onClick={() => onComplete(lesson.id)}>Дуусгах</button>
<button onClick={() => onDelete(lesson.id)}>Устгах</button>
</td>
</tr>
);
});
function LessonTable({ lessons }: { lessons: Lesson[] }) {
const [completedIds, setCompletedIds] = useState<Set<number>>(new Set());
// ❌ Render бүрт шинэ функц — memo-г тойрч гардаг
const handleComplete = (id: number) => {
setCompletedIds((prev) => new Set([...prev, id]));
};
// ✅ useCallback — функцийн reference тогтмол хэвээр байна
const handleCompleteCallback = useCallback((id: number) => {
setCompletedIds((prev) => new Set([...prev, id]));
}, []); // dependency хоосон — хэзээ ч өөрчлөгдөхгүй
const handleDelete = useCallback((id: number) => {
console.log("Устгах:", id);
}, []);
return (
<table>
<tbody>
{lessons.map((lesson) => (
<LessonRow
key={lesson.id}
lesson={lesson}
onComplete={handleCompleteCallback}
onDelete={handleDelete}
/>
))}
</tbody>
</table>
);
}
Оновчлолын алтан дүрэм: Эхлээд ажиллуулж, удаан болоход нь л оновчлол хий. React.memo, useMemo, useCallback бүгдийг нэмэх нь код нарийн төвөгтэй болгохоос өөр үр дүнгүй байж болно. Profiler-ийн хэмжилтийг үндэслэн, нотлогдсон удаашрал дээр л оновчлол хий.
Дараагийн хичээлд:
Compound Component Pattern — нэг component-н дотор хэд хэдэн холбогдох дэд component-уудыг нэгтгэн уян хатан API бүтээх аргыг практикаар судална.