diff --git a/PERFORMANCE.md b/PERFORMANCE.md new file mode 100644 index 0000000..37c95c9 --- /dev/null +++ b/PERFORMANCE.md @@ -0,0 +1,63 @@ +# Performance Report + +## Before Optimization + +### 1. Toggle columns +- Commit duration: 19.8ms +- Render duration: 961.9ms +- Screenshot: ![Toggle columns](./src/screenshots/screenshots_toggleing_columns.png) + +### 2. Searching +- Commit duration: 15.8ms +- Render duration: 486.3ms +- Screenshot: ![Searching](./src/screenshots/screenshots_searching_country.png) + +### 3. Select year +- Commit duration: 5.7ms +- Render duration: 559.3ms +- Screenshot: ![Select year](./src/screenshots/screenshot_select_year.png) + +### 4. Sorting +- Commit duration: 5.4ms +- Render duration: 462.7ms +- Screenshot: ![Sorting](./src/screenshots/screenshot_sortyng_by.png) + +## After optimization + +### 1. Toggle columns +- Commit duration: 5.1ms +- Render duration: 29.4ms +- Screenshot: ![Toggle columns](./src/screenshots/Toggleing_columns_after_perf.png) + +### 2. Searching +- Commit duration: 4.7ms +- Render duration: 87ms +- Screenshot: ![Searching](./src/screenshots/Searching_after_perf.png) + +### 3. Select year +- Commit duration: 8ms +- Render duration: 34.3ms +- Screenshot: ![Select year](./src/screenshots/Select_year_after_pref.png) + +### 4. Sorting +- Commit duration: 4.6ms +- Render duration: 94.8ms +- Screenshot: ![Sorting](./src/screenshots/Sortyng_by_after_perf.png) + + +## Comparison + +| Action | Before | After | Improvement | +|---|---|---|---| +| Toggle columns | 961.9ms | 29.4ms | ~97% | +| Searching | 486.3ms | 87ms | ~82% | +| Select year | 559.3ms | 34.3ms | ~94% | +| Sorting | 462.7ms | 94.8ms | ~80% | + +## Applied optimizations + +- `useMemo` — memoized filtered/sorted country list and computed values in CountryCard +- `useCallback` — memoized event handlers in App +- `React.memo` — prevented unnecessary re-renders of CountryCard +- Proper `key` props — used `country.id` instead of index +- Virtualization — rendered only visible rows with `react-window` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 89aafd4..a8f8531 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "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", @@ -59,7 +60,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 +1526,6 @@ "integrity": "sha512-68e+T28EbdmLSTkPgs3+UacC6rzmqrcWFPQs1C8mwJhI/r5Uxr0yEuQotczNRROd1gq30NGxee+fo0rSIxpyAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1537,7 +1536,6 @@ "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1607,7 +1605,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 +1856,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1978,7 +1974,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2200,7 +2195,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 +3013,6 @@ "integrity": "sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==", "dev": true, "license": "MIT", - "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -3066,7 +3059,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 +3068,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 +3640,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 +3676,6 @@ "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -3866,7 +3866,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3968,7 +3967,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -4098,7 +4096,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..757f147 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,8 @@ }, "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", diff --git a/src/components/app/app.tsx b/src/components/app/app.tsx index 6ae81df..24ff6b8 100644 --- a/src/components/app/app.tsx +++ b/src/components/app/app.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { useCo2Data } from '../../hooks/useCo2Data'; import { LoadingSpinner } from '../loading-spinner/loading-spinner'; import { SearchBar } from '../search-bar/search-bar'; @@ -35,37 +35,37 @@ export const App = () => { 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 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 ; diff --git a/src/components/country-card/country-card.tsx b/src/components/country-card/country-card.tsx index 7eb1059..a9da1b7 100644 --- a/src/components/country-card/country-card.tsx +++ b/src/components/country-card/country-card.tsx @@ -8,6 +8,7 @@ import { import { formatNumber } from '../../utils/format-utils'; import styles from './country-card.module.css'; +import { useMemo, memo } from 'react'; type CountryCardProps = { country: Country; @@ -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(function CountryCard({ country, selectedYear, selectedColumns }: CountryCardProps) { + const yearDataMap = useMemo(() => { + return createYearDataMap(country.data);}, [country.data]) + const population = useMemo(() => { + return getPopulationForYear(yearDataMap, selectedYear); + }, [yearDataMap, selectedYear]) + const co2 = useMemo(() => { + return getCo2ForYear(yearDataMap, selectedYear); + }, [yearDataMap, selectedYear]) return (
@@ -39,4 +45,4 @@ export const CountryCard = ({ country, selectedYear, selectedColumns }: CountryC
); -}; +}); diff --git a/src/components/country-list/country-list.tsx b/src/components/country-list/country-list.tsx index d3a43e5..0f86dee 100644 --- a/src/components/country-list/country-list.tsx +++ b/src/components/country-list/country-list.tsx @@ -3,6 +3,9 @@ import { CountryCard } from '../country-card/country-card'; import { getPopulationForYear, createYearDataMap } from '../../utils/data-transformers'; import styles from './country-list.module.css'; +import { useMemo } from 'react'; +import { List } from 'react-window'; +import type { RowComponentProps } from 'react-window'; type CountryListProps = { countries: Country[]; @@ -15,6 +18,31 @@ type CountryListProps = { onYearChange: (year: number) => void; }; +type RowProps = { + filteredCountries: Country[]; + selectedYear: number; + selectedColumns: string[]; +}; + +const Row = ({ + index, + style, + filteredCountries, + selectedYear, + selectedColumns, +}: RowComponentProps) => { + const country = filteredCountries[index]; + return ( +
+ +
+ ); +}; + export const CountryList = ({ countries, searchQuery, @@ -24,32 +52,33 @@ export const CountryList = ({ 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; - } - }); + const filteredCountries = useMemo(() => { + return 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; + } + }); + }, [countries, searchQuery, selectedRegion, selectedYear, sortField, sortOrder]); return (
- {filteredCountries.map((country, index) => ( - - ))} +
); }; diff --git a/src/screenshots/Searching_after_perf.png b/src/screenshots/Searching_after_perf.png new file mode 100644 index 0000000..722af90 Binary files /dev/null and b/src/screenshots/Searching_after_perf.png differ diff --git a/src/screenshots/Select_year_after_pref.png b/src/screenshots/Select_year_after_pref.png new file mode 100644 index 0000000..3d708c8 Binary files /dev/null and b/src/screenshots/Select_year_after_pref.png differ diff --git a/src/screenshots/Sortyng_by_after_perf.png b/src/screenshots/Sortyng_by_after_perf.png new file mode 100644 index 0000000..2b1e947 Binary files /dev/null and b/src/screenshots/Sortyng_by_after_perf.png differ diff --git a/src/screenshots/Toggleing_columns_after_perf.png b/src/screenshots/Toggleing_columns_after_perf.png new file mode 100644 index 0000000..9e71381 Binary files /dev/null and b/src/screenshots/Toggleing_columns_after_perf.png differ diff --git a/src/screenshots/screenshot_select_year.png b/src/screenshots/screenshot_select_year.png new file mode 100644 index 0000000..90f511b Binary files /dev/null and b/src/screenshots/screenshot_select_year.png differ diff --git a/src/screenshots/screenshot_sortyng_by.png b/src/screenshots/screenshot_sortyng_by.png new file mode 100644 index 0000000..e82de92 Binary files /dev/null and b/src/screenshots/screenshot_sortyng_by.png differ diff --git a/src/screenshots/screenshots_searching_country.png b/src/screenshots/screenshots_searching_country.png new file mode 100644 index 0000000..c1eff00 Binary files /dev/null and b/src/screenshots/screenshots_searching_country.png differ diff --git a/src/screenshots/screenshots_toggleing_columns.png b/src/screenshots/screenshots_toggleing_columns.png new file mode 100644 index 0000000..74a6389 Binary files /dev/null and b/src/screenshots/screenshots_toggleing_columns.png differ