Как повысить производительность рендеринга больших сеток в React?

Я создаю реакцию + машинопись + вайт Nonogram App.

Вы можете увидеть недостаточную производительность для больших размеров сетки в живой демонстрации ghpages: https://timonkobusch.github.io/nonogram/ Вот полный код для него: https://github.com/timonkobusch/nonogram

Мой вопрос и идеи:

  1. Есть ли вообще способ реалистично обрабатывать элементы большого размера в React с высокой производительностью без необходимости использовать здесь библиотеку анимации? (Это не то, чего я хочу, если честно)
  2. Поможет ли оптимизация или же это будут очень небольшие улучшения, незначительные и незаметные?
  3. Стоит ли мне рассмотреть возможность извлечения большего количества атрибутов из класса нонограмм, чтобы повысить производительность и уменьшить количество повторных рендерингов? Как бы я сделал это эффективно.
  4. Может ли глобальное государственное управление быть хорошим решением? Как редукс (не уверен)?
  5. Может быть, переключиться на сетку и обработать больше стилей внутри CSS, если это возможно?

Вот два наиболее важных файла, рендеринг которых (очевидно) занимает больше всего времени: Нонограммагрид.tsx:

const NonogramGrid = ({
    nonogram,
    onMouseDownHandler,
    onMouseOverHandler,
    gameRunning,
}: INonogramGridProps) => {
    const [selectedCell, setSelectedCell] = useState<{
        row: number;
        column: number;
    }>({ row: -1, column: -1 });

    const handleMouseDown =
        (row: number, col: number) => (e: React.MouseEvent) => {
            e.preventDefault();
            if (!gameRunning || nonogram.progress.isWon) return;
            onMouseDownHandler(row, col);
        };

    const handleMouseOver =
        (row: number, col: number) => (e: React.MouseEvent) => {
            e.preventDefault();
            if (!gameRunning || nonogram.progress.isWon) return;
            setSelectedCell({ row, column: col });
            onMouseOverHandler(row, col);
        };

    const handleMouseLeave = () => {
        setSelectedCell({ row: -1, column: -1 });
    };

    const validSizes = [5, 10, 15, 20, 25];
    const sizeClass = validSizes.includes(nonogram.size)
        ? `grid-${nonogram.size}x${nonogram.size}`
        : "grid-15x15";

    return (
        <div className = {`content ${sizeClass}`}>
            <Hints
                hintLines = {nonogram.hints.rows}
                finishedLines = {nonogram.finishedLines.rows}
                gameRunning = {gameRunning && !nonogram.progress.isWon}
                classIdentifier = {"left-hints"}
            />
            <table>
                <Hints
                    hintLines = {nonogram.hints.columns}
                    finishedLines = {nonogram.finishedLines.columns}
                    gameRunning = {gameRunning && !nonogram.progress.isWon}
                    classIdentifier = {"top-hints"}
                />
                <tbody className = "table">
                    {Array.from({ length: nonogram.size }).map((_, row) => {
                        return (
                            <tr key = {row} className = "row">
                                {Array.from({ length: nonogram.size }).map(
                                    (_, col) => {
                                        const fifthRowBorder =
                                            row === 4 ? "fifth-row" : "";
                                        const highlighted =
                                            selectedCell.row === row ||
                                            selectedCell.column === col
                                                ? "highlighted"
                                                : "";
                                        const hideCell = gameRunning
                                            ? ""
                                            : "hide-cell";
                                        const gameWon = nonogram.progress.isWon
                                            ? "game-won"
                                            : "";
                                        let colored = "empty";
                                        switch (nonogram.grid[row][col]) {
                                            case 1:
                                                colored = "colored";
                                                break;
                                            case 2:
                                                colored = "crossed";
                                                break;
                                            case 3:
                                                colored = "wrongColored";
                                                break;
                                            case 4:
                                                colored = "wrongCrossed";
                                                break;
                                        }
                                        return (
                                            <td
                                                key = {col}
                                                className = {`cell ${fifthRowBorder}`}
                                                onMouseDown = {handleMouseDown(
                                                    row,
                                                    col
                                                )}
                                                onMouseOver = {handleMouseOver(
                                                    row,
                                                    col
                                                )}
                                                onMouseLeave = {handleMouseLeave}
                                            >
                                                {row > 0 && row % 5 === 0 && (
                                                    <div className = "fifth-row-border"></div>
                                                )}
                                                {col > 0 && col % 5 === 0 && (
                                                    <div className = "fifth-col-border"></div>
                                                )}
                                                <div
                                                    id = "cell"
                                                    className = {`${colored} ${highlighted} ${hideCell} ${gameWon}`}
                                                ></div>
                                            </td>
                                        );
                                    }
                                )}
                            </tr>
                        );
                    })}
                </tbody>
            </table>
        </div>
    );
};

export default NonogramGrid;

И App.tsx, который обрабатывает состояние игры и нонограмму:

import "components/App.scss";
import NonogramGrid from "./NonogramGrid/NonogramGrid";
import { Nonogram } from "modules/Nonogram";
import { useEffect, useState } from "react";
import GameController from "components/GameController/GameController";
import PlayController from "components/PlayController/PlayController";
import useTimer from "components/utils/useTimer";
import AppHeader from "components/AppHeader/AppHeader";
import About from "components/About/About";
import workerURL from "./utils/createClassWorker?worker&url";

const enum MarkLock {
    UNSET = 0,
    ROW = 1,
    COL = 2,
}

const App = () => {
    const [nonogram, setNonogram] = useState(() => new Nonogram(10));
    const [nonogramHistory, setNonogramHistory] = useState<Nonogram[]>([]);
    const [mouseDown, setMouseDown] = useState(false);
    const [marking, setMarking] = useState(true);
    const [loading, setLoading] = useState(false);
    const [clearMode, setClearMode] = useState(false);
    const { seconds, resetTimer, startTimer, pauseTimer } = useTimer();
    const [gameRunning, setgameRunning] = useState(false);

    const [clickCoords, setClickCoords] = useState({ row: 0, column: 0 });
    const [markLock, setMarkLock] = useState(MarkLock.UNSET);

    useEffect(() => {
        const handleMouseUp = () => {
            if (mouseDown) {
                setMouseDown(false);
            }
        };
        const handleFKeyPress = (e: KeyboardEvent) => {
            if (e.key === "f") {
                setMarking(!marking);
            }
        };
        document.addEventListener("mouseup", handleMouseUp);
        document.addEventListener("keypress", handleFKeyPress);
        return () => {
            document.removeEventListener("mouseup", handleMouseUp);
            document.removeEventListener("keypress", handleFKeyPress);
        };
    });

    const handleUndo = () => {
        ...
    };

    const handleMouseDown = (x: number, y: number) => {
        ...
    };

    const handleMouseOver = (x: number, y: number) => {
        ...
    };
    const handleGenerate = (size: number) => {
        ...
    };

    const handleReset = () => {
        ...
    };

    const handlePause = () => {
        ...
    };
    return (
        <div className = "App">
            <AppHeader />
            <div className = "App-content">
                <div className = "ControlField">
                    <GameController
                        handleGenerate = {handleGenerate}
                        handleReset = {handleReset}
                        gameWon = {nonogram.progress.isWon}
                        loading = {loading}
                    />
                    <PlayController
                        progress = {nonogram.progress}
                        seconds = {seconds}
                        handlePause = {handlePause}
                        gameRunning = {gameRunning}
                        handleUndo = {handleUndo}
                        undoActive = {nonogramHistory.length > 0}
                        marking = {marking}
                        toggleMarking = {() => setMarking(!marking)}
                    />
                    <About />
                </div>
                <NonogramGrid
                    nonogram = {nonogram}
                    onMouseDownHandler = {handleMouseDown}
                    onMouseOverHandler = {handleMouseOver}
                    gameRunning = {gameRunning}
                />
            </div>
        </div>
    );
};

export default App;

Я искал проблему и в основном находил решения с помощью окна реакции, но здесь это не решение, потому что сетка всегда полностью видна.

Если у вас есть опыт работы с этим, не могли бы вы дать мне указания, как вы это решили? Является ли использование библиотеки анимации решением?

🤔 А знаете ли вы, что...
React поддерживает однонаправленный поток данных (unidirectional data flow) для предсказуемого управления состоянием.


146
1

Ответ:

Решено

Я вижу, что есть проблема с таймером - таймер реализован внутри хука useTimer, который импортируется в компонент приложения. При каждом перерисовке пользовательского перехвата компонент, в который импортируется перехват, также перерисовывается. Это означает, что каждую секунду происходит новый рендеринг. Я написал код, запоминающий компонент Nonogramgrid, потому что там много дорогостоящих итераций. Вот изменения:

const memoizedNonogramGrid = useMemo(() => {
    console.info("rerender the grid")
    return (
        <NonogramGrid
            nonogram = {nonogram}
            onMouseDownHandler = {handleMouseDown}
            onMouseOverHandler = {handleMouseOver}
            gameRunning = {gameRunning}
        />
    )
}, [gameRunning, handleMouseDown, handleMouseOver, nonogram])

const handleMouseDown = useMemo(() => {
    return (x: number, y: number) => {
        setNonogramHistory([...nonogramHistory, new Nonogram(nonogram)]);

        const updatedGrid = new Nonogram(nonogram);
        updatedGrid.click(x, y, marking);
        setNonogram(updatedGrid);

        // Set mouse state
        setMouseDown(true);
        setClickCoords({ row: y, column: x });
        setMarkLock(MarkLock.UNSET);
        if (nonogram.grid[x][y] === 0) {
            setClearMode(false);
        } else {
            setClearMode(true);
        }
        if (updatedGrid.progress.isWon) {
            pauseTimer();
        }
    };
}, [nonogram, marking, nonogramHistory, pauseTimer])

const handleMouseOver = useMemo(() => {
    return (x: number, y: number) => {
        if (mouseDown) {
            if (x === clickCoords.column && y === clickCoords.row) {
                return;
            }
            if (markLock === MarkLock.UNSET) {
                if (x === clickCoords.column) {
                    setMarkLock(MarkLock.COL);
                } else if (y === clickCoords.row) {
                    setMarkLock(MarkLock.ROW);
                }
            }
            if (markLock === MarkLock.COL) {
                x = clickCoords.column;
            } else if (markLock === MarkLock.ROW) {
                y = clickCoords.row;
            }

            const updatedGrid = new Nonogram(nonogram as Nonogram);
            updatedGrid.setCell(x, y, marking, clearMode);
            setNonogram(updatedGrid);
            if (updatedGrid.progress.isWon) {
                pauseTimer();
            }
        }
    };
}, [clearMode, clickCoords.column, clickCoords.row, markLock, nonogram, mouseDown, marking, pauseTimer])

const pauseTimer = useMemo(() => {
    return () => {
        setActive(false);
    };
}, [])

Конечно, запоминание можно выполнить с помощью хука useCallback.

Второе изменение, которое я сделал, — это передача начального значения в состояние нонограммы, поскольку хорошей практикой является передача не функции, а только значения, поскольку при каждом повторном рендеринге происходит выполнение этой функции. Другими словами, ЦП создает новую нонограмму (10). Вот код:

const initialNonogramState = new Nonogram(10)

const App = () => {
    const [nonogram, setNonogram] = useState(initialNonogramState);

Далее, по-прежнему существует проблема с компонентом сетки Nonogram — при каждом наведении курсора мыши происходит повторный рендеринг компонента, и каждый повторный рендеринг создает новые итерации. Сейчас я рассматриваю некоторую логику для создания компонентов и простого изменения реквизита. Меня вдохновила библиотека React-Data-Table-Component, так как у них есть хорошая функция запоминания таблиц.