diff --git a/src/components/section.tsx b/src/components/section.tsx index debc4f3..846de01 100644 --- a/src/components/section.tsx +++ b/src/components/section.tsx @@ -1,8 +1,9 @@ "use client"; -import { ReactNode, useEffect, useRef, useState } from "react"; +import { memo, ReactNode, useEffect, useRef, useState } from "react"; -export const Section = ({ +// ⚡ Optimization: Section component is memoized to prevent redundant re-renders of the section wrapper. +export const Section = memo(({ id, title, children, @@ -62,4 +63,6 @@ export const Section = ({ ); -}; +}); + +Section.displayName = "Section"; diff --git a/src/components/sections/education.tsx b/src/components/sections/education.tsx index 34c6ee1..b88a43a 100644 --- a/src/components/sections/education.tsx +++ b/src/components/sections/education.tsx @@ -4,9 +4,14 @@ import Image from "next/image"; import degreesData from "@/data/degrees"; +import { memo } from "react"; + import { Section } from "../section"; -export const EducationSection = () => { +// ⚡ Optimization: EducationSection is memoized to prevent unnecessary re-renders. +// Since it only depends on static data (degreesData) and doesn't consume filter/search contexts, +// it should only re-render if its parent (Home) re-renders, which we also want to avoid. +export const EducationSection = memo(() => { return (
{degreesData.map((degree) => ( @@ -36,4 +41,6 @@ export const EducationSection = () => { ))}
); -}; +}); + +EducationSection.displayName = "EducationSection"; diff --git a/src/components/sections/skills.tsx b/src/components/sections/skills.tsx index bd8b3c9..206d510 100644 --- a/src/components/sections/skills.tsx +++ b/src/components/sections/skills.tsx @@ -14,19 +14,25 @@ export const SkillsSection = () => { const { debouncedQuery } = useSearch(); const { selected } = useFilter(); - const filteredSkills = useMemo(() => { - const lowercaseQuery = debouncedQuery.toLowerCase(); + const areas = selected["areas"]; - // Determine which skills are matching the area filter - const selectedAreas = selected["areas"] || []; - const areaMatchingSkills = new Set(); - if (selectedAreas.length === 0) { - Object.keys(skillsData).forEach((s) => areaMatchingSkills.add(s)); + // ⚡ Optimization: Memoize areaMatchingSkills separately from the query filter. + // This avoids re-calculating the matching skills set when only the search query changes. + const areaMatchingSkills = useMemo(() => { + const matchingSkills = new Set(); + if (!areas || areas.length === 0) { + Object.keys(skillsData).forEach((s) => matchingSkills.add(s)); } else { - selectedAreas.forEach((area) => { - areaSkills[area]?.forEach((skill) => areaMatchingSkills.add(skill)); + areas.forEach((area) => { + areaSkills[area]?.forEach((skill) => matchingSkills.add(skill)); }); } + return matchingSkills; + }, [areas]); + + // ⚡ Optimization: filteredSkills now only depends on debouncedQuery and the memoized areaMatchingSkills. + const filteredSkills = useMemo(() => { + const lowercaseQuery = debouncedQuery.toLowerCase(); return Object.entries(skillsData).filter(([key, skill]) => { const matchesQuery = @@ -34,7 +40,7 @@ export const SkillsSection = () => { const matchesArea = areaMatchingSkills.has(key); return matchesQuery && matchesArea; }); - }, [debouncedQuery, selected]); + }, [debouncedQuery, areaMatchingSkills]); return (