Esta página es mi primer intento de usar Webassembly (Wasm) en Next.js. Para eso primero seguí este tutorial que implementa El Juego de la vida en Rust y lo compila a Wasm y busqué cómo podía incorporarlo. Por suerte Next tiene soporte para Wasm.
El Juego de la vida (Game of life) es un autómata celular diseñado por el matemático británico John Horton Conway en 1970. Es un juego de cero jugadores, y consiste en una grilla infinita (como la de la página anterior) donde cada casillero representa una "célula" que puede encontrarse en uno de dos estados: viva o muerta, poblada o despoblada, encendida o apagada, etc. Una vez establecido el patrón inicial solamente queda ver cómo evoluciona según las siguientes reglas:
  1. Cualquier célula viva con menos de dos vecinas vivas muere en la siguiente generación
  2. Cualquier célula viva con dos o tres vecinas vivas sigue viva
  3. Cualquier célula viva con más de tres vecinas vivas muere
  4. Cualquier célula muerta con exactamente tres vecinas vivas pasa a estar viva
Según MDN web docs Webassembly es:

"un nuevo tipo de código que puede ser ejecutado en navegadores modernos, y provee nuevas funcionalidades y mejoras en rendimiento. No está pensado para ser ser escrito a mano, si no que está diseñado par ser un objeto final de compilación para lenguajes de bajo nivel como C, C++, Rust, etc."

Un ejemplo reciente de las posibilidades que da es la mejora de rendimiento de TensorFlow.js gracias a Wasm:

Rust tiene un buen soporte de Webassembly, con herramientas como wasm-bindgen que ayudan en la comunicación con Javascript.
Next.js es un framework basado en React que permite hacer páginas web híbridas tanto estáticas como renderizadas en el lado del servidor (SSR).

Algunos puntos

Una vez que completamos el tutorial de Wasm y tenemos la implementación del Juego de la vida la primer pregunta que surge si uno quire usar Next.js es ¿cómo se integra?

Comunicación con Wasm

Dentro del repositorio de Next.js vamos a encontrar un ejemplo que se llama "with-webassembly" donde se muestra cómo crear un componente con Wasm. Nos interesa el archivo index.js donde podemos ver cómo se hace:

import { withRouter } from 'next/router'
import dynamic from 'next/dynamic'
import Link from 'next/link'

const RustComponent = dynamic({
  loader: async () => {
    // Import the wasm module
    const rustModule = await import('../add.wasm')
    // Return a React component that calls the add_one method on the wasm module
    return (props) => <div>{rustModule.add_one(props.number)}</div>
  },
})

const Page = ({ router: { query } }) => {
  const number = parseInt(query.number || 30)
  return (
    <div>
      <RustComponent number={number} />
      <Link href={`/? number = ${number + 1}`}>
        <a>+</a>
      </Link>
    </div>
  )
} 
Podemos ver que hay una linea con el path al archivo wasm: await import('../add.wasm') (o carpeta con archivos) y usa dynamic import. A través de la variable rustModule vamos a poder acceder a las funciones que hayamos hecho públicas. La clave está en que el llamado a dynamic() devuelve un componente React donde vamos a poder usar esas funciones.

Animación del juego

El tutorial de Wasm implementa una primer solución usando requestAnimationFrame() para animar el juego pero como Next.js está basado en React después de unos intentos usé setInterval() junto con useState para provocar el re renderizado incrementando un contador por cada generación. Por otro lado el universo del juego está implementado usando un struct mutable que se modifica cada llamado a la función tick(), que calcula cada nueva generación.

Componente que se comunica con Wasm:


const GameOfLife = dynamic({
  loader: async () => {
    const rust = await import('../pkg')

    // Return the React component using Webassembly
    return (
      (props) => {
        // useRef: if universe is used directly doen't work
        const universe = props.pattern
          ? useRef(rust.Universe.new_pattern(props.width, props.height, props.pattern))
          : useRef(rust.Universe.new(props.width, props.height))
        const [state, setState] = useState(0);

        useEffect(() => {
          var timerID = setInterval(() => setState(state + 1), 250);
          universe.current.tick();
          return function cleanup() {
            clearInterval(timerID);
          };
        });

        return (
          <>
            <pre className={styles.gol}>{universe.current.render()}</pre>
          </>
        )
      }
    )
    // end of component
  },
})

TODO

Además de limpiar y mejorar muchas cosas algunas ideas para intentar:
  • Modificar la función de Rust new() para que acepte un patrón para empezar el juego (en parte implementado en new_pattern())
  • Permitir ingresar un patron clickendo en una grilla en blanco
  • Implementar un botón de "start" y "stop"
  • Agregar una galería de patrones como Glider