diff --git a/PERFORMANCE.md b/PERFORMANCE.md new file mode 100644 index 0000000..8bf4e0b --- /dev/null +++ b/PERFORMANCE.md @@ -0,0 +1,96 @@ +# Performance Optimization Report + +Profiling was performed in **development mode** using the React `` API (same metrics as React DevTools Profiler: `actualDuration` = render duration, `commitTime - startTime` = commit duration). Each interaction was recorded in a separate session after the initial data load completed. + +## Optimizations Applied + +| Technique | Where applied | +| --------- | ------------- | +| `useMemo` | `filteredCountries`, `years`, `availableColumns`, `yearDataMap`, `population`, `co2`, `record` | +| `useCallback` | All event handlers in `App` (`handleSearch`, `handleYearChange`, `handleSortFieldChange`, etc.) | +| `React.memo` | `CountryList`, `CountryCard`, `DataTable`, `SearchBar`, `YearSelector`, `ColumnModal` | +| Proper `key` props | `country.id` for list items, `column` for table rows, `yearOption` for year options | +| Virtualization | `react-window` `List` component in `CountryList` (renders only visible rows) | + +--- + +## Baseline Measurements + +### Interaction A: Sort countries + +- **Commit duration**: 0.89 s +- **Render duration**: 882.2 ms +- **Screenshot**: ![Sort countries baseline](screenshots/baseline/sort-countries.png) + +> Toggling sort order re-rendered all ~250 `CountryCard` components. The flame chart showed `CountryList` → `CountryCard` → `DataTable` as the dominant render path. + +### Interaction B: Search countries + +- **Commit duration**: 0.12 s +- **Render duration**: 118.7 ms +- **Screenshot**: ![Search countries baseline](screenshots/baseline/search-countries.png) + +> Typing "United" still triggered a full list reconciliation because `CountryCard` components used array index as `key`, causing unnecessary DOM updates even for filtered results. + +### Interaction C: Change year + +- **Commit duration**: 0.83 s +- **Render duration**: 822.3 ms +- **Screenshot**: ![Change year baseline](screenshots/baseline/change-year.png) + +> Changing the year selector caused every visible country card and its data table to re-render, recalculating `createYearDataMap` on each card independently. + +### Interaction D: Toggle column + +- **Commit duration**: 1.20 s +- **Render duration**: 1188.5 ms +- **Screenshot**: ![Toggle column baseline](screenshots/baseline/toggle-column.png) + +> Toggling a column in the modal re-rendered all country cards because handler references changed on every `App` render and components were not memoized. + +--- + +## Optimized Measurements + +### Interaction A: Sort countries + +- **Commit duration**: 0.29 s +- **Render duration**: 294.1 ms +- **Screenshot**: ![Sort countries optimized](screenshots/optimized/sort-countries.png) + +### Interaction B: Search countries + +- **Commit duration**: 0.02 s +- **Render duration**: 18.0 ms +- **Screenshot**: ![Search countries optimized](screenshots/optimized/search-countries.png) + +### Interaction C: Change year + +- **Commit duration**: 0.04 s +- **Render duration**: 41.2 ms +- **Screenshot**: ![Change year optimized](screenshots/optimized/change-year.png) + +### Interaction D: Toggle column + +- **Commit duration**: 0.02 s +- **Render duration**: 19.3 ms +- **Screenshot**: ![Toggle column optimized](screenshots/optimized/toggle-column.png) + +--- + +## Summary of Improvements + +| Interaction | Baseline (ms) | Optimized (ms) | Improvement | +| ---------------- | ------------- | -------------- | ----------- | +| Sort countries | 882.2 | 294.1 | 66.7% | +| Search countries | 118.7 | 18.0 | 84.8% | +| Change year | 822.3 | 41.2 | 95.0% | +| Toggle column | 1188.5 | 19.3 | 98.4% | +| **Average** | **752.9** | **93.2** | **87.6%** | + +### Key findings + +1. **Virtualization** had the largest impact on sort and year-change interactions, reducing rendered nodes from ~250 cards to ~3–6 visible cards. +2. **`React.memo` + `useCallback`** prevented the entire component tree from re-rendering when toggling columns — only affected `CountryCard` instances updated. +3. **`useMemo`** for `filteredCountries` eliminated redundant filter/sort work and stabilized list references passed to the virtualized list. +4. **Stable `key` props** (`country.id` instead of array index) improved reconciliation when the filtered list changed during search. diff --git a/package-lock.json b/package-lock.json index 89aafd4..ac41b66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,15 @@ "version": "0.0.0", "dependencies": { "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-window": "^2.2.7" }, "devDependencies": { "@eslint/js": "^9.39.1", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", + "@types/react-window": "^1.8.8", "@vitejs/plugin-react": "^5.1.1", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", @@ -59,7 +61,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1526,7 +1527,6 @@ "integrity": "sha512-68e+T28EbdmLSTkPgs3+UacC6rzmqrcWFPQs1C8mwJhI/r5Uxr0yEuQotczNRROd1gq30NGxee+fo0rSIxpyAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1537,7 +1537,6 @@ "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1562,6 +1561,16 @@ "@types/react": "*" } }, + "node_modules/@types/react-window": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", + "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.54.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", @@ -1607,7 +1616,6 @@ "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.54.0", "@typescript-eslint/types": "8.54.0", @@ -1859,7 +1867,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1978,7 +1985,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2200,7 +2206,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -3019,7 +3024,6 @@ "integrity": "sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -3066,7 +3070,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -3076,7 +3079,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3649,6 +3651,16 @@ "dev": true, "license": "MIT" }, + "node_modules/react-window": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.7.tgz", + "integrity": "sha512-SH5nvfUQwGHYyriDUAOt7wfPsfG9Qxd6OdzQxl5oQ4dsSsUicqQvjV7dR+NqZ4coY0fUn3w1jnC5PwzIUWEg5w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3675,7 +3687,6 @@ "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -3866,7 +3877,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3968,7 +3978,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -4098,7 +4107,6 @@ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 204350f..196e9e1 100644 --- a/package.json +++ b/package.json @@ -13,13 +13,15 @@ }, "dependencies": { "react": "^19.2.0", - "react-dom": "^19.2.0" + "react-dom": "^19.2.0", + "react-window": "^2.2.7" }, "devDependencies": { "@eslint/js": "^9.39.1", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", + "@types/react-window": "^1.8.8", "@vitejs/plugin-react": "^5.1.1", "eslint": "^9.39.1", "eslint-plugin-react-hooks": "^7.0.1", diff --git a/screenshots/baseline/change-year.png b/screenshots/baseline/change-year.png new file mode 100644 index 0000000..debbcaa Binary files /dev/null and b/screenshots/baseline/change-year.png differ diff --git a/screenshots/baseline/search-countries.png b/screenshots/baseline/search-countries.png new file mode 100644 index 0000000..37de85b Binary files /dev/null and b/screenshots/baseline/search-countries.png differ diff --git a/screenshots/baseline/sort-countries.png b/screenshots/baseline/sort-countries.png new file mode 100644 index 0000000..321c8ea Binary files /dev/null and b/screenshots/baseline/sort-countries.png differ diff --git a/screenshots/baseline/toggle-column.png b/screenshots/baseline/toggle-column.png new file mode 100644 index 0000000..614bcc6 Binary files /dev/null and b/screenshots/baseline/toggle-column.png differ diff --git a/screenshots/optimized/change-year.png b/screenshots/optimized/change-year.png new file mode 100644 index 0000000..d7015b9 Binary files /dev/null and b/screenshots/optimized/change-year.png differ diff --git a/screenshots/optimized/search-countries.png b/screenshots/optimized/search-countries.png new file mode 100644 index 0000000..a062273 Binary files /dev/null and b/screenshots/optimized/search-countries.png differ diff --git a/screenshots/optimized/sort-countries.png b/screenshots/optimized/sort-countries.png new file mode 100644 index 0000000..b5bf2ed Binary files /dev/null and b/screenshots/optimized/sort-countries.png differ diff --git a/screenshots/optimized/toggle-column.png b/screenshots/optimized/toggle-column.png new file mode 100644 index 0000000..fc529b8 Binary files /dev/null and b/screenshots/optimized/toggle-column.png differ diff --git a/src/components/app/app.tsx b/src/components/app/app.tsx index 6ae81df..5a9d892 100644 --- a/src/components/app/app.tsx +++ b/src/components/app/app.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useMemo, useCallback } from 'react'; import { useCo2Data } from '../../hooks/useCo2Data'; import { LoadingSpinner } from '../loading-spinner/loading-spinner'; import { SearchBar } from '../search-bar/search-bar'; @@ -32,40 +32,40 @@ export const App = () => { isColumnModalOpen: false, }); - const years = data ? getAvailableYears(data) : []; - const availableColumns = getAvailableColumns(); - - const handleSearch = (value: string) => { - setState({ ...state, searchQuery: value }); - }; - - const handleYearChange = (year: number) => { - setState({ ...state, selectedYear: year }); - }; - - const handleSortFieldChange = (field: 'name' | 'population') => { - setState({ ...state, sortField: field }); - }; - - const handleSortOrderToggle = () => { - setState({ - ...state, - sortOrder: state.sortOrder === 'asc' ? 'desc' : 'asc', - }); - }; - - const handleColumnToggle = (column: string) => { - setState({ - ...state, - selectedColumns: state.selectedColumns.includes(column) - ? state.selectedColumns.filter((c) => c !== column) - : [...state.selectedColumns, column], - }); - }; - - const handleModalToggle = () => { - setState({ ...state, isColumnModalOpen: !state.isColumnModalOpen }); - }; + const years = useMemo(() => (data ? getAvailableYears(data) : []), [data]); + const availableColumns = useMemo(() => getAvailableColumns(), []); + + const handleSearch = useCallback((value: string) => { + setState((prev) => ({ ...prev, searchQuery: value })); + }, []); + + const handleYearChange = useCallback((year: number) => { + setState((prev) => ({ ...prev, selectedYear: year })); + }, []); + + const handleSortFieldChange = useCallback((field: 'name' | 'population') => { + setState((prev) => ({ ...prev, sortField: field })); + }, []); + + const handleSortOrderToggle = useCallback(() => { + setState((prev) => ({ + ...prev, + sortOrder: prev.sortOrder === 'asc' ? 'desc' : 'asc', + })); + }, []); + + const handleColumnToggle = useCallback((column: string) => { + setState((prev) => ({ + ...prev, + selectedColumns: prev.selectedColumns.includes(column) + ? prev.selectedColumns.filter((c) => c !== column) + : [...prev.selectedColumns, column], + })); + }, []); + + const handleModalToggle = useCallback(() => { + setState((prev) => ({ ...prev, isColumnModalOpen: !prev.isColumnModalOpen })); + }, []); if (isLoading) { return ; @@ -83,7 +83,6 @@ export const App = () => {

CO₂ Emissions Data Explorer

- {/* Controls */}
@@ -111,7 +110,6 @@ export const App = () => {
- {/* Country List */} { selectedYear={state.selectedYear} sortField={state.sortField} sortOrder={state.sortOrder} - onYearChange={handleYearChange} /> - {/* Column Modal */} void; }; -export const ColumnModal = ({ - isOpen, - availableColumns, - selectedColumns, - onToggle, - onClose, -}: ColumnModalProps) => { - if (!isOpen) { - return null; - } +export const ColumnModal = memo( + ({ isOpen, availableColumns, selectedColumns, onToggle, onClose }: ColumnModalProps) => { + if (!isOpen) { + return null; + } - return ( -
-
-

Select columns to display

-
- {availableColumns.map((column) => ( -
- -
- ))} -
-
- + return ( +
+
+

Select columns to display

+
+ {availableColumns.map((column) => ( +
+ +
+ ))} +
+
+ +
-
- ); -}; + ); + } +); + +ColumnModal.displayName = 'ColumnModal'; diff --git a/src/components/country-card/country-card.tsx b/src/components/country-card/country-card.tsx index 7eb1059..5157187 100644 --- a/src/components/country-card/country-card.tsx +++ b/src/components/country-card/country-card.tsx @@ -1,3 +1,4 @@ +import { memo, useMemo } from 'react'; import type { Country } from '../../types'; import { DataTable } from '../data-table/data-table'; import { @@ -15,10 +16,15 @@ type CountryCardProps = { selectedColumns: string[]; }; -export const CountryCard = ({ country, selectedYear, selectedColumns }: CountryCardProps) => { - const yearDataMap = createYearDataMap(country.data); - const population = getPopulationForYear(yearDataMap, selectedYear); - const co2 = getCo2ForYear(yearDataMap, selectedYear); +export const CountryCard = memo(({ country, selectedYear, selectedColumns }: CountryCardProps) => { + const yearDataMap = useMemo(() => createYearDataMap(country.data), [country.data]); + + const population = useMemo( + () => getPopulationForYear(yearDataMap, selectedYear), + [yearDataMap, selectedYear] + ); + + const co2 = useMemo(() => getCo2ForYear(yearDataMap, selectedYear), [yearDataMap, selectedYear]); return (
@@ -39,4 +45,6 @@ export const CountryCard = ({ country, selectedYear, selectedColumns }: CountryC
); -}; +}); + +CountryCard.displayName = 'CountryCard'; diff --git a/src/components/country-list/country-list.tsx b/src/components/country-list/country-list.tsx index d3a43e5..6c6bc69 100644 --- a/src/components/country-list/country-list.tsx +++ b/src/components/country-list/country-list.tsx @@ -1,9 +1,14 @@ +import { memo, useMemo } from 'react'; +import { List, type RowComponentProps } from 'react-window'; import type { Country } from '../../types'; import { CountryCard } from '../country-card/country-card'; import { getPopulationForYear, createYearDataMap } from '../../utils/data-transformers'; import styles from './country-list.module.css'; +const CARD_HEIGHT = 280; +const LIST_HEIGHT = 600; + type CountryListProps = { countries: Country[]; searchQuery: string; @@ -12,44 +17,86 @@ type CountryListProps = { selectedYear: number; sortField: 'name' | 'population'; sortOrder: 'asc' | 'desc'; - onYearChange: (year: number) => void; }; -export const CountryList = ({ +type RowProps = { + countries: Country[]; + selectedYear: number; + selectedColumns: string[]; +}; + +const CountryRow = ({ + index, + style, countries, - searchQuery, - selectedColumns, - selectedRegion, selectedYear, - sortField, - sortOrder, -}: CountryListProps) => { - const filteredCountries = countries - .filter((c) => { - const matchesSearch = c.id.toLowerCase().includes(searchQuery.toLowerCase()); - const matchesRegion = !selectedRegion || c.data.some((d) => d.region === selectedRegion); - return matchesSearch && matchesRegion; - }) - .sort((a, b) => { - if (sortField === 'name') { - return sortOrder === 'asc' ? a.id.localeCompare(b.id) : b.id.localeCompare(a.id); - } else { - const popA = getPopulationForYear(createYearDataMap(a.data), selectedYear) || 0; - const popB = getPopulationForYear(createYearDataMap(b.data), selectedYear) || 0; - return sortOrder === 'asc' ? popA - popB : popB - popA; - } - }); + selectedColumns, +}: RowComponentProps) => { + const country = countries[index]; return ( -
- {filteredCountries.map((country, index) => ( - - ))} +
+
); }; + +export const CountryList = memo( + ({ + countries, + searchQuery, + selectedColumns, + selectedRegion, + selectedYear, + sortField, + sortOrder, + }: CountryListProps) => { + const filteredCountries = useMemo(() => { + const query = searchQuery.toLowerCase(); + + return countries + .filter((c) => { + const matchesSearch = c.id.toLowerCase().includes(query); + const matchesRegion = + !selectedRegion || c.data.some((d) => d.region === selectedRegion); + return matchesSearch && matchesRegion; + }) + .sort((a, b) => { + if (sortField === 'name') { + return sortOrder === 'asc' ? a.id.localeCompare(b.id) : b.id.localeCompare(a.id); + } + + const popA = getPopulationForYear(createYearDataMap(a.data), selectedYear) || 0; + const popB = getPopulationForYear(createYearDataMap(b.data), selectedYear) || 0; + return sortOrder === 'asc' ? popA - popB : popB - popA; + }); + }, [countries, searchQuery, selectedRegion, selectedYear, sortField, sortOrder]); + + const rowProps = useMemo( + () => ({ + countries: filteredCountries, + selectedYear, + selectedColumns, + }), + [filteredCountries, selectedYear, selectedColumns] + ); + + return ( +
+ +
+ ); + } +); + +CountryList.displayName = 'CountryList'; diff --git a/src/components/data-table/data-table.tsx b/src/components/data-table/data-table.tsx index 1050543..d31f471 100644 --- a/src/components/data-table/data-table.tsx +++ b/src/components/data-table/data-table.tsx @@ -1,3 +1,4 @@ +import { memo, useMemo } from 'react'; import type { YearData } from '../../types'; import { formatNumber } from '../../utils/format-utils'; @@ -9,20 +10,21 @@ type DataTableProps = { columns: string[]; }; -export const DataTable = ({ data, year, columns }: DataTableProps) => { - const yearData = data.filter((d) => d.year === year); +export const DataTable = memo(({ data, year, columns }: DataTableProps) => { + const record = useMemo(() => { + const yearData = data.filter((d) => d.year === year); + return yearData.length > 0 ? yearData[0] : null; + }, [data, year]); - if (yearData.length === 0) { + if (!record) { return
No data available for year {year}
; } - const record = yearData[0]; - return ( - {columns.map((column, index) => ( - + {columns.map((column) => ( +
{column.replace(/_/g, ' ').toUpperCase()} {formatNumber(record[column as keyof YearData] as number | undefined, { @@ -34,4 +36,6 @@ export const DataTable = ({ data, year, columns }: DataTableProps) => {
); -}; +}); + +DataTable.displayName = 'DataTable'; diff --git a/src/components/search-bar/search-bar.tsx b/src/components/search-bar/search-bar.tsx index 87260cc..28bfdc4 100644 --- a/src/components/search-bar/search-bar.tsx +++ b/src/components/search-bar/search-bar.tsx @@ -1,3 +1,4 @@ +import { memo } from 'react'; import styles from './search-bar.module.css'; type SearchBarProps = { @@ -5,7 +6,7 @@ type SearchBarProps = { onChange: (value: string) => void; }; -export const SearchBar = ({ value, onChange }: SearchBarProps) => { +export const SearchBar = memo(({ value, onChange }: SearchBarProps) => { return (
); -}; +}); + +SearchBar.displayName = 'SearchBar'; diff --git a/src/components/year-selector/year-selector.tsx b/src/components/year-selector/year-selector.tsx index 5142b2b..6a60df0 100644 --- a/src/components/year-selector/year-selector.tsx +++ b/src/components/year-selector/year-selector.tsx @@ -1,3 +1,4 @@ +import { memo } from 'react'; import styles from './year-selector.module.css'; type YearSelectorProps = { @@ -6,7 +7,7 @@ type YearSelectorProps = { onChange: (year: number) => void; }; -export const YearSelector = ({ year, years, onChange }: YearSelectorProps) => { +export const YearSelector = memo(({ year, years, onChange }: YearSelectorProps) => { return (
); -}; +}); + +YearSelector.displayName = 'YearSelector';