Modo concurrente en React: adaptación de aplicaciones web para dispositivos y velocidad de Internet

En este artículo, presentaré el modo concurrente en React. Averigüemos qué es: cuáles son las características, qué nuevas herramientas han aparecido y cómo optimizar el funcionamiento de las aplicaciones web con su ayuda para que todo pueda volar para los usuarios. El modo concurrente es una nueva característica de React. Su tarea es adaptar la aplicación a diferentes dispositivos y velocidades de red. Hasta ahora, el modo concurrente es un experimento que los desarrolladores de la biblioteca pueden cambiar, lo que significa que no hay nuevas herramientas en el establo. Te lo advertí y ahora vámonos.



Actualmente, existen dos limitaciones para los componentes de renderizado: potencia del procesador y velocidad de transferencia de datos de la red. Siempre que es necesario mostrar algo al usuario, la versión actual de React intenta representar cada componente de principio a fin. No importa si la interfaz puede congelarse durante unos segundos. Lo mismo ocurre con la transferencia de datos. React esperará absolutamente todos los datos que necesita el componente, en lugar de dibujarlos pieza por pieza.







El régimen competitivo resuelve estos problemas. Con él, React puede pausar, priorizar e incluso deshacer operaciones que anteriormente estaban bloqueadas, por lo que en modo concurrente puede comenzar a renderizar componentes independientemente de si se han recibido todos los datos o solo parte de ellos.



El modo concurrente es la arquitectura de fibra



El modo competitivo no es algo nuevo que los desarrolladores de repente decidieron agregar, y todo funcionó allí. Preparado para su lanzamiento con antelación. En la versión 16, el motor React se cambió a una arquitectura de fibra, que, en principio, se parece al programador de tareas del sistema operativo. El planificador distribuye recursos computacionales entre procesos. Es capaz de cambiar en cualquier momento, por lo que el usuario tiene la ilusión de que los procesos se ejecutan en paralelo.



La arquitectura de fibra hace lo mismo, pero con componentes. A pesar de que ya está en React, la arquitectura de Fiber parece estar en animación suspendida y no usa sus capacidades al máximo. El modo competitivo lo encenderá a plena potencia.



Cuando actualiza un componente en modo normal, debe dibujar un marco completamente nuevo en la pantalla. Hasta que se complete la actualización, el usuario no verá nada. En este caso, React funciona sincrónicamente. La fibra usa un concepto diferente. Cada 16 ms, hay una interrupción y una verificación: ¿ha cambiado el árbol virtual, han aparecido nuevos datos? Si es así, el usuario los verá inmediatamente.



¿Por qué 16ms? Los desarrolladores de React se esfuerzan por volver a dibujar la pantalla a una velocidad cercana a los 60 cuadros por segundo. Para ajustar 60 actualizaciones en 1000 ms, debe realizarlas aproximadamente cada 16 ms. De ahí la figura. El modo competitivo sale de la caja y agrega nuevas herramientas que mejoran la vida del front-end. Te hablaré de cada uno en detalle.



Suspenso



Suspense se introdujo en React 16.6 como un mecanismo para cargar componentes dinámicamente. En modo concurrente, esta lógica se conserva, pero aparecen oportunidades adicionales. El suspenso se convierte en un mecanismo que funciona junto con la biblioteca de carga de datos. Solicitamos un recurso especial a través de la biblioteca y leemos datos de ella.



Suspense lee simultáneamente datos que aún no están listos. ¿Cómo? Solicitamos los datos, y hasta que vengan completos, ya estamos empezando a leerlos en pequeños fragmentos. Lo mejor para los desarrolladores es administrar el orden en que se muestran los datos cargados. Suspense le permite mostrar los componentes de la página de forma simultánea e independiente entre sí. Hace que el código sea sencillo: basta con mirar la estructura de Suspense para ver en qué orden se solicitan los datos.



Una solución típica para cargar páginas en React "antiguo" es Fetch-On-Render. En este caso, solicitamos datos después de renderizar dentro de useEffect o componentDidMount. Esta es la lógica estándar cuando no hay Redux u otra capa de datos. Por ejemplo, queremos dibujar 2 componentes, cada uno de los cuales necesita datos:



  • Solicitud de componente 1
  • Expectativa…
  • Obtener datos -> renderizar componente 1
  • Solicitud de componente 2
  • Expectativa…
  • Obtener datos -> renderizar componente 2


En este enfoque, el siguiente componente se solicita solo después de que se procesa el primero. Es largo e incómodo.



Consideremos otra forma, Fetch-Then-Render: primero solicitamos todos los datos, luego renderizamos la página.



  • Solicitud de componente 1
  • Solicitud de componente 2
  • Expectativa…
  • Obteniendo el componente 1
  • Obteniendo el componente 2
  • Representación de componentes


En este caso, movemos el estado de la solicitud hacia arriba, lo delegamos a la biblioteca para trabajar con datos. El método funciona muy bien, pero hay un matiz. Si uno de los componentes tarda mucho más en cargar que el otro, el usuario no verá nada, aunque ya podríamos mostrarle algo. Veamos un código de muestra de la demostración con 2 componentes: Usuario y Publicaciones. Envolvemos componentes en Suspense:



const resource = fetchData() // -    React
function Page({ resource }) {
    return (
        <Suspense fallback={<h1>Loading user...</h1>}>
            <User resource={resource} />
            <Suspense fallback={<h1>Loading posts...</h1>}>
                <Posts resource={resource} />
            </Suspense>
        </Suspense>
    )
}


Puede parecer que este enfoque está cerca de Fetch-On-Render, cuando solicitamos datos después de renderizar el primer componente. Pero de hecho, usar Suspense obtendrá los datos mucho más rápido. Esto se debe al hecho de que ambas solicitudes se envían en paralelo.



En Suspense, puede especificar la reserva, el componente que desea mostrar y pasar el recurso implementado por la biblioteca de recuperación de datos dentro del componente. Lo usamos como está. Dentro de los componentes, solicitamos datos del recurso y llamamos al método de lectura. Ésta es la promesa que nos hace la biblioteca. Suspense entenderá si los datos se han cargado y, de ser así, lo mostrará.



Tenga en cuenta que los componentes están intentando leer datos que aún están en proceso de recepción:



function User() {
    const user = resource.user.read()
    return <h1>{user.name}</h1>
}
function Posts() {
    const posts = resource.posts.read()
    return //  
}


En las demostraciones actuales de Dan Abramov, tal cosa se utiliza como código auxiliar de un recurso .



read() {
    if (status === 'pending') {
        throw suspender
    } else if (status === 'error') {
        throw result
    } else if (status === 'success') {
        return result
    }
}




Si el recurso aún se está cargando, lanzamos el objeto Promise como una excepción. Suspense detecta esta excepción, se da cuenta de que es una promesa y continúa cargando. Si, en lugar de una Promesa, llega una excepción con cualquier otro objeto, quedará claro que la solicitud terminó por error. Cuando se devuelva el resultado final, Suspense lo mostrará. Es importante para nosotros obtener un recurso y llamar a un método sobre él. Cómo se implementa internamente es una decisión de los desarrolladores de la biblioteca, lo principal es que Suspense entiende su implementación.



¿Cuándo solicitar datos? Preguntar en la parte superior del árbol no es una buena idea, porque es posible que nunca sean necesarios. Una mejor opción es hacer esto de inmediato al navegar dentro de los controladores de eventos. Por ejemplo, obtenga el estado inicial a través de un enlace y luego realice una solicitud de recursos tan pronto como el usuario haga clic en el botón.



Así es como se verá en el código:



function App() {
    const [resource, setResource] = useState(initialResource)
    return (
        <>
            <Button text='' onClick={() => {
                setResource(fetchData())
            }}>
            <Page resource={resource} />
        </>
    );
}


El suspenso es increíblemente flexible. Se puede utilizar para mostrar componentes uno tras otro.



return (
    <Suspense fallback={<h1>Loading user...</h1>}>
        <User />
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
        </Suspense>
    </Suspense>
)


O al mismo tiempo, ambos componentes deben estar envueltos en un Suspenso.



return (
    <Suspense fallback={<h1>Loading user and posts...</h1>}>
        <User />
        <Posts />
    </Suspense>
)


O bien, cargue los componentes por separado envolviéndolos en Suspensión independiente. El recurso se cargará a través de la biblioteca. Es muy guay y conveniente.



return (
    <>
        <Suspense fallback={<h1>Loading user...</h1>}>
            <User />
        </Suspense>
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
        </Suspense>
    </>
)


Además, los componentes de Límite de error detectarán errores dentro de Suspense. Si algo salió mal, podemos mostrar que el usuario ha cargado, pero las publicaciones no, y dar un error.



return (
    <Suspense fallback={<h1>Loading user...</h1>}>
        <User resource={resource} />
        <ErrorBoundary fallback={<h2>Could not fetch posts</h2>}>
            <Suspense fallback={<h1>Loading posts...</h1>}>
                <Posts resource={resource} />
            </Suspense>
        </ErrorBoundary>
    </Suspense>
)


Ahora echemos un vistazo a otras herramientas que pueden dar rienda suelta a todos los beneficios del régimen competitivo.



SuspenseList



SuspenseList ayuda al mismo tiempo a controlar el orden de carga de Suspense. Si tuviéramos que cargar varios Suspense estrictamente uno tras otro sin él, tendrían que estar anidados uno dentro del otro:



return (
    <Suspense fallback={<h1>Loading user...</h1>}>
        <User />
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
            <Suspense fallback={<h1>Loading facts...</h1>}>
                <Facts />
            </Suspense>
        </Suspense>
    </Suspense>
)


SuspenseList lo hace mucho más fácil:



return (
    <SuspenseList revealOrder="forwards" tail="collapsed">
        <Suspense fallback={<h1>Loading posts...</h1>}>
            <Posts />
        </Suspense>
        <Suspense fallback={<h1>Loading facts...</h1>}>
            <Facts />
        </Suspense>
    </Suspense>
)


La flexibilidad de SuspenseList es asombrosa. Puede anidar SuspenseList entre sí como desee y personalizar el orden de carga en el interior, ya que será conveniente para mostrar widgets y cualquier otro componente.



useTransition



Un gancho especial que pospone la actualización del componente hasta que esté completamente listo y elimina el estado de carga intermedio. ¿Para qué sirve? React se esfuerza por hacer la transición lo más rápido posible al cambiar de estado. Pero a veces es importante tomarse su tiempo. Si una parte de los datos se carga en una acción del usuario, generalmente en el momento de la carga mostramos un cargador o esqueleto. Si los datos llegan muy rápido, el cargador no tendrá tiempo de completar ni siquiera medio giro. Parpadeará, luego desaparecerá y dibujaremos el componente actualizado. En tales casos, es más prudente no mostrar el cargador en absoluto.



Aquí es donde entra useTransition. ¿Cómo funciona en código? Llamamos al gancho useTransition y especificamos el tiempo de espera en milisegundos. Si los datos no llegan dentro del tiempo especificado, seguiremos mostrando el cargador. Pero si los obtenemos más rápido, habrá una transición instantánea.



function App() {
    const [resource, setResource] = useState(initialResource)
    const [startTransition, isPending] = useTransition({ timeoutMs: 2000 })
    return <>
        <Button text='' disabled={isPending} onClick={() => {
            startTransition(() => {
                setResource(fetchData())
            })
        }}>
        <Page resource={resource} />
    </>
}


A veces, cuando vamos a la página, no queremos mostrar el cargador, pero aún tenemos que cambiar algo en la interfaz. Por ejemplo, mientras dure la transición, bloquee el botón. Entonces, la propiedad isPending será útil: le informará que estamos en la etapa de transición. Para el usuario, la actualización será instantánea, pero es importante notar aquí que la magia useTransition solo afecta a los componentes envueltos en Suspense. UseTransition en sí no funcionará.



Las transiciones son comunes en las interfaces. La lógica responsable de la transición sería genial para coserla en el botón e integrarla en la biblioteca. Si hay un componente responsable de las transiciones entre páginas, puede ajustar el onClick que se pasa a través de los accesorios al botón en handleClick y mostrar el estado isDisabled.



function Button({ text, onClick }) {
    const [startTransition, isPending] = useTransition({ timeoutMs: 2000 })

    function handleClick() {
        startTransition(() => {
            onClick()
        })
    }

    return <button onClick={handleClick} disabled={isPending}>text</button>
}


useDeferredValue



Entonces, hay un componente con el que hacemos transiciones. A veces surge la siguiente situación: el usuario quiere ir a otra página, hemos recibido algunos de los datos y estamos listos para mostrarlos. Al mismo tiempo, las páginas difieren ligeramente entre sí. En este caso, sería lógico mostrar al usuario los datos obsoletos hasta que se cargue todo lo demás.



Ahora React no puede hacer esto: en la versión actual, solo los datos del estado actual se pueden mostrar en la pantalla del usuario. Pero useDeferredValue en modo concurrente puede devolver una versión diferida del valor, mostrar datos desactualizados en lugar de un cargador parpadeante o un respaldo en el momento del arranque. Este gancho toma el valor para el que queremos la versión diferida y el retraso en milisegundos.



La interfaz se vuelve súper fluida. Las actualizaciones se pueden realizar con una cantidad mínima de datos y todo lo demás se carga gradualmente. El usuario tiene la impresión de que la aplicación es rápida y fluida. En acción, useDeferredValue se ve así:



function Page({ resource }) {
    const deferredResource = useDeferredValue(resource, { timeoutMs: 1000 })
    const isDeferred = resource !== deferredResource;
    return (
        <Suspense fallback={<h1>Loading user...</h1>}>
            <User resource={resource} />
            <Suspense fallback={<h1>Loading posts...</h1>}>
                <Posts resource={deferredResource} isDeferred={isDeferred}/>
            </Suspense>
        </Suspense>
    )
}


Puede comparar el valor de los accesorios con el obtenido a través de useDeferredValue. Si difieren, la página todavía se está cargando.



Curiosamente, useDeferredValue le permitirá repetir el truco de carga diferida, no solo para los datos que se transmiten a través de la red, sino también para eliminar la congelación de la interfaz debido a grandes cálculos.



¿Por qué es genial? Los diferentes dispositivos funcionan de manera diferente. Si ejecuta una aplicación con useDeferredValue en un nuevo iPhone, la transición de una página a otra será instantánea, incluso si las páginas son pesadas. Pero cuando se usa antirrebote, el retraso aparecerá incluso en un dispositivo potente. UseDeferredValue y el modo concurrente se adaptan al hardware: si funciona lentamente, la entrada seguirá volando y la página en sí se actualizará según lo permita el dispositivo.



¿Cómo cambio un proyecto al modo concurrente?



El modo competitivo es un modo, por lo que debe habilitarlo. Como un interruptor de palanca que hace que Fiber funcione a plena capacidad. Por donde empiezas



Eliminamos el legado. Eliminamos todos los métodos obsoletos en el código y nos aseguramos de que no estén en las bibliotecas. Si la aplicación funciona bien en React.StrictMode, entonces todo está bien, el movimiento será fácil. La complicación potencial son los problemas dentro de las bibliotecas. En este caso, debe actualizar a una nueva versión o cambiar la biblioteca. O abandonar el régimen competitivo. Después de deshacerse del legado, todo lo que queda es cambiar de raíz.



Con la llegada del modo concurrente, estarán disponibles tres modos de conexión raíz:



  • El modo de

    ReactDOM.render(<App />, rootNode)

    renderizado antiguo quedará obsoleto una vez que se lance el modo competitivo.
  • Modo de bloqueo

    ReactDOM.createBlockingRoot(rootNode).render(<App />)

    Como etapa intermedia, se agregará un modo de bloqueo, que da acceso a una parte de las oportunidades del modo competitivo en proyectos donde existen legados u otras dificultades con la reubicación.
  • Modo competitivo

    ReactDOM.createRoot(rootNode).render(<App />)

    Si todo está bien, no hay un legado y el proyecto se puede cambiar de inmediato, reemplace el renderizado en el proyecto con createRoot, y listo para un futuro brillante.


conclusiones



Las operaciones de bloqueo dentro de React se hacen asincrónicas al cambiar a Fiber. Están surgiendo nuevas herramientas que facilitan la adaptación de la aplicación tanto a las capacidades del dispositivo como a la velocidad de la red:



  • Suspenso, gracias al cual puede especificar el orden de carga de los datos.
  • SuspenseList, que lo hace aún más conveniente.
  • useTransition para crear transiciones suaves entre componentes envueltos en suspenso.
  • useDeferredValue: para mostrar datos obsoletos durante las actualizaciones de E / S y componentes


Intente experimentar con el modo concurrente mientras aún esté apagado. El modo concurrente le permite lograr resultados impresionantes: carga rápida y suave de componentes en cualquier orden conveniente, interfaz superfluida. Los detalles se describen en la documentación, también hay demostraciones con ejemplos que debería estudiar por su cuenta. Y si tiene curiosidad sobre cómo funciona la arquitectura de fibra, aquí tiene un enlace a una charla interesante.



Evalúe sus proyectos: ¿qué se puede mejorar con las nuevas herramientas? Y cuando se acabe el régimen competitivo, no dude en moverse. ¡Todo será genial!



All Articles