3 puntos por GN⁺ 4 시간 전 | 1 comentarios | Compartir por WhatsApp
  • Linear maneja el trabajo de gestión de issues con una base de datos dentro del navegador y sincronización local-first, por lo que las actualizaciones de issues se reflejan en la UI en cuestión de milisegundos
  • La base de datos real que lee la UI está en IndexedDB; los cambios se aplican primero en local, luego se envían de forma asíncrona al servidor, y este vuelve a distribuir los deltas por WebSocket
  • La carga inicial usa una estrategia que reduce la espera de red con poco envío de JavaScript y CSS, división agresiva de código, modulepreload, precaché con service worker y app shell inline
  • El motor de sincronización hidrata los datos de IndexedDB en un pool de objetos de MobX, guarda los cambios en una cola de transacciones y vuelve a renderizar solo las celdas necesarias con estado observable a nivel de campo
  • La sensación de velocidad es el resultado de un diseño de sistema que combina entrada centrada en teclado, paleta global de comandos, animaciones amigables con la GPU y tiempos de transición cortos

La base de datos dentro del navegador

  • En una web app CRUD tradicional, después del clic del usuario pasan una solicitud HTTP, la consulta a la base de datos del servidor, la recepción de la respuesta y el repintado del navegador, lo que provoca durante cientos de milisegundos spinners, skeletons o una UI congelada
  • Linear coloca en IndexedDB del navegador la base de datos real que lee la UI, aplica primero los cambios en local y luego los envía de forma asíncrona al servidor; el servidor transmite los deltas a otros clientes por WebSocket
  • En una web app rápida, el mayor cuello de botella es la red, y transferir datos entre cliente y servidor cuesta cientos de milisegundos
  • El flujo central de Linear consiste en hacer invisibles para el usuario las solicitudes de red y eliminar, en lo posible, los estados de carga
// Linear
issue.title = "Faster app launch";
issue.save();
  • issue.title = "Faster app launch" actualiza el almacén de datos en memoria; en el caso de Linear, usa observables de MobX
  • issue.save(); pone en cola una transacción que el motor de sincronización agrupa y luego envía al servidor
  • La UI se vuelve a renderizar de forma sincrónica con base en los cambios de memoria local, y como la sincronización de datos ocurre en segundo plano, no hacen falta spinners
  • Tuomas dijo en una conferencia de 2024 que el primer código que escribió en Linear fue el motor de sincronización, y lo describió como un enfoque poco común en una startup
  • La mayoría de las apps no necesitan crear su propio motor de sincronización como Linear; solo con las actualizaciones optimistas de TanStack Query y SWR ya es posible acercarse bastante a esa sensación de velocidad
  • Las solicitudes optimistas ofrecen una gran mejora al eliminar spinners innecesarios, actualizar el estado de inmediato, validar en segundo plano y hacer rollback si es necesario
  • La capacidad de respuesta de la UI no debería depender de la latencia de red, y la velocidad que percibe el usuario está determinada más por la respuesta de la interfaz que por la del servidor
  • Un vistazo al stack de Linear

    • Linear está construido sobre un stack simple como React, TypeScript, MobX, Postgres y CDN
    • El frontend usa React y react-dom, MobX, TypeScript, Rolldown-Vite y plugin-react-oxc, ProseMirror y y-prosemirror, Radix UI primitives, Emotion y StyleX, Comlink, idb, graphql-request, Sentry e Inter Variable
    • El backend usa Node.js y TypeScript, PostgreSQL sobre Cloud SQL, Memorystore Redis, turbopuffer, Kubernetes de GCP y Cloudflare Workers
    • El cliente de escritorio está basado en Electron, y móvil se reimplementó por completo por separado con Swift para iOS y Kotlin
    • El sitio de marketing usa Next.js, styled-components e inline SVG sprite
    • Linear mantiene el renderizado del lado del cliente, y muestra que con la arquitectura y el diseño correctos, el CSR también puede sentirse inmediato
    • Mantener toda la app del lado del cliente da un modelo mental más simple al reducir complejidades como la separación entre servidor y cliente, si se puede acceder a window o cómo configurar encabezados de caché

Hacer que la primera carga se sienta instantánea

  • El tiempo que tarda una herramienta de productividad en permitir empezar a trabajar de verdad es un detalle importante
  • La carga inicial de una app del lado del cliente puede volverse lenta por la secuencia de solicitud de index.html, solicitudes de JavaScript y CSS, procesamiento de autenticación y solicitudes de API para mostrar la app
  • Flujo del bundler de Linear: Parcel, Rollup, Vite, Rolldown

    • La sensación de inmediatez empieza en el tiempo de compilación, antes del runtime, y para lograr cargas rápidas es importante reducir la cantidad de JavaScript y CSS que se envía
    • Linear reescribió su pipeline de build en el orden Parcel → Rollup → Vite → Rolldown, y cada cambio tuvo como objetivo reducir la cantidad de JavaScript y CSS y mejorar la experiencia de los desarrolladores
    • Cifras de mejora según el blog de Linear
    • 50% menos código transferido
    • 30% menos tamaño después de la compresión
    • Mejora de 10~30% en la carga de página con caché fría
    • 59% menos Time-to-first-paint en la vista de active-issues en Safari
    • 70~80% menos uso de memoria
    • Buena parte de la mejora proviene de la combinación de apuntar solo a navegadores modernos, mejor dead-code elimination y una división de código agresiva
    • Abandonar el soporte legado trajo grandes beneficios, al eliminar polyfills, transpilar a ES5 y el fallback de nomodule
    • Incluso después de la optimización, Linear sigue enviando alrededor de 21 MB de JavaScript minificado, pero lo divide agresivamente en cientos de chunks a nivel de ruta para traerlos solo cuando se necesitan
    • La clave no es elegir un bundler específico, sino eliminar navegadores legados, migrar a ESM nativo y aplicar una división de código agresiva
    • Al acumularse estos pasos, el JavaScript de la primera carga de Linear se reduce aproximadamente a la mitad y los tiempos de build bajan no solo un poco, sino por un orden de magnitud
  • Precarga después de la carga inicial

    • Al dividir JavaScript en chunks pequeños, aparece el problema de una carga en cascada donde cada chunk importa otros chunks
    • Linear hace que el navegador vea la lista completa y empiece las solicitudes en paralelo antes de ejecutar el JavaScript, de modo que cuando el script de entrada llega al primer import, los chunks relacionados ya estén en caché
    • Al hacer coincidir modulepreload en el <head> con el valor crossorigin del script de entrada, el navegador no trata la precarga y el import como recursos separados, sino que reutiliza el fetch almacenado en caché
    • La línea de tiempo de una carga fría cambia de una cascada secuencial a un solo lote en paralelo, y el trabajo de red sigue existiendo, pero se realiza de una sola vez
    • Este trabajo se hace en segundo plano cuando el usuario llega por primera vez a la página de inicio de sesión, y unos segundos después toda la app queda guardada en caché para poder entregarse de inmediato
    Publicidad
  • Service worker para más velocidad y funciones offline

    • Los chunks a nivel de ruta de vistas que el usuario aún no ha visitado son almacenados en caché en segundo plano por el service worker
    • El service worker tiene un precache manifest integrado en el código fuente y cubre alrededor de 1,200 assets con hash, incluidos chunks de ruta, íconos y fuentes
    • La estructura está diseñada para que toda la app entre en caché unos segundos después de llegar a la pantalla de inicio de sesión
    • A partir de ahí, la navegación omite por completo la red, y el service worker responde directamente desde su propia caché sin pasar por la caché HTTP
    • Combinado con un motor de sincronización local-first y los datos del usuario ya guardados en IndexedDB, Linear puede usarse incluso sin conexión
    • Soporta leer issues, crear nuevos issues, editar título y descripción y cambiar el estado
    • Todas las acciones se encolan en un almacén local de transacciones y se vacían cuando vuelve la conexión
    • modulepreload es el mecanismo para traer en paralelo lo que se necesita ahora y evitar que el navegador se bloquee en una cadena serial de imports
    • El service worker es el mecanismo para preparar lo que se necesitará después
    • La estrategia de carga rápida de Linear consiste en eliminar la mayor cantidad posible de código, dividirlo en partes pequeñas y hacer precache en segundo plano; el objetivo es acelerar las solicitudes de red o eliminarlas por completo
  • Estructura del bundle vendor

    • Cada paquete que usa Linear se divide en un chunk separado y se almacena en caché de forma independiente
    • El vendor.js tradicional invalida la caché de todo el grafo de dependencias aunque solo se actualice una dependencia
    • La división de chunks de Linear crea un caché vendor granular en lugar de un solo archivo grande, por lo que al actualizar una dependencia solo se invalida ese chunk y el resto permanece en caché
  • Carga de archivos de fuentes grandes

    • Una carga incorrecta de fuentes puede provocar texto temporalmente invisible, layout shift al cambiar a la fuente real y fetches duplicados por desajustes en la precarga
    • Linear hace preload de la fuente Inter Variable en el <head> y preconnect a static.linear.app
    <link rel="preload"
          href="https://static.linear.app/fonts/InterVariable.woff2?v=4.1";
          as="font" type="font/woff2" crossorigin="anonymous">
    <link rel="preconnect" href="https://static.linear.app"; crossorigin>
    
    • La variable font cubre todo el eje de pesos de 100~900 con un solo woff2, eliminando solicitudes por cada peso
    • font-display: swap renderiza de inmediato la pila fallback y luego la reemplaza cuando termina de cargar Inter
    • El crossorigin="anonymous" de la etiqueta preload es una configuración clave para que el navegador reutilice el recurso en caché cuando el CSS haga referencia a la misma fuente después
    • Si no hay crossorigin, el modo CORS de la precarga y el de la referencia desde CSS difieren, y eso hace que el navegador vuelva a descargar la fuente
  • App shell inline

    • Linear inserta CSS inline dentro del <head> suficiente para dibujar el estado de carga y así mostrar el app shell sin solicitar una hoja de estilos externa
    • El JavaScript inline ejecuta de inmediato la lógica condicional necesaria para la experiencia inicial
    • Detecta Electron y el user agent de Linear para agregar la clase electron
    • Si no existe localStorage.ApplicationStore, agrega la clase logged-out
    • Restaura desde localStorage.splashScreenConfig tokens del shell como el fondo de la barra lateral, el ancho de la barra lateral y el modo oscuro
    • Si el usuario configuró abrir enlaces en la app de escritorio, ajusta el ancho de la barra lateral a 8px
    • Antes de que el primer bundle de JavaScript llegue por la red, la pantalla de carga ya tiene el tema, el tamaño y la posición ajustados según si el usuario inició sesión o no
    • La forma más rápida de hacer que la app se sienta lista apenas el usuario presiona Enter tras escribir la URL es entregar el app shell en la respuesta inicial de index.html
    Publicidad
  • Renderizar primero y autenticar después

    • Un flujo de autenticación típico avanza en el orden fetch de HTML, carga de bundles, validación de sesión, fetch de usuario, fetch de workspace y renderizado, y puede tardar de 1 a 3 segundos antes de que el usuario vea algo
    • Linear trata la autenticación de la misma manera que el procesamiento de cambios: asume el camino normal y valida en segundo plano
    • La mayoría de las apps CRUD guardan la sesión real en una cookie HttpOnly y, para que el frontend sepa durante el arranque si el usuario inició sesión, agregan otra cookie legible desde JavaScript o una solicitud a /me
    • El script inline de arranque de Linear no usa una señal de autenticación en paralelo, sino que solo comprueba la existencia de localStorage.ApplicationStore
    if (localStorage.getItem("ApplicationStore") === null) {
      document.documentElement.classList.add("logged-out");
    }
    
    • Si existe ApplicationStore, significa que el usuario ya ha usado Linear en este navegador y que los datos del workspace ya están en IndexedDB
    • Si el valor no existe, no hay datos que renderizar, así que el shell cambia al layout logged-out y continúa el flujo de inicio de sesión
    • El token de sesión real está en la cookie, y el bundle no intenta determinar de antemano el estado de la sesión
    • Si el handshake de WebSocket, un delta de sincronización o una llamada HTTP recibe un 401 por una sesión expirada, el cliente redirige al inicio de sesión
    • El patrón completo consiste en confiar en los datos locales para renderizar de inmediato, dejar al servidor como fuente de verdad para la corrección y coordinar ambos lados de forma asíncrona

Motor de sincronización

  • La velocidad de Linear parte de la decisión de ver al servidor no como la source of truth de la UI, sino como un objetivo de sincronización
  • La velocidad no es un solo elemento, sino el resultado de tres ejes que trabajan juntos
  • 1. Los datos ya están ahí

    • Al iniciar la app, no trae el workspace desde el servidor, sino que lo hidrata desde IndexedDB hacia un pool de objetos MobX en memoria
    • Todas las consultas de la UI apuntan primero al pool de objetos, y como los issues ya están en el dispositivo del usuario, no existe el estado de “loading issues”
    • A medida que creció, Linear fragmentó los datos del motor de sincronización con un principio parecido al de los bundles de JavaScript
    • Las dos tablas más pesadas, Issue y Comment, no se traen de una vez, sino que se hidratan de forma diferida cuando hacen falta
    • Este enfoque es una división de código a nivel de datos, y hace que el costo de inicio dependa no del tamaño del workspace, sino de su estructura
    • Un workspace con 10,000 issues inicia casi con la misma velocidad que uno con 100 issues
    • Al entrar a un proyecto, los issues ya están ahí, y al filtrar por assignee, el índice ya está construido
  • 2. Los cambios no esperan a la red

    • Cuando cambia el estado de un issue, pasan tres cosas casi al mismo tiempo
    • La actualización de un observable de MobX refleja el cambio en la UI
    • El cambio se registra en una cola de transacciones duraderas de IndexedDB
    • El cambio se agrega a la cola de envío al servidor
    • En este punto, la red todavía no se ha usado
    • El usuario no espera para ver sus propios cambios, y los reintentos, rollback y recarga con persistencia se manejan en segundo plano
    • Si el servidor lo rechaza, el observable vuelve atrás y se produce un breve flicker, pero la mayoría de los cambios inválidos se detectan antes de crear la transacción
    • El flujo de Linear empieza con el cambio local y trata al servidor no como una etapa de autorización, sino de confirmación
  • 3. Un delta, una celda

    • Cuando el servidor confirma el cambio del usuario o el cambio de otra persona, vuelve al cliente un pequeño sobre JSON que indica qué se movió
    • El cliente aplica el cambio escribiendo el valor en ese observable de MobX
    • Cada propiedad de cada modelo en Linear es un observable, y todos los componentes que leen esa propiedad están envueltos con observer()
    • MobX puede saber con precisión de qué campos depende cada componente
    • Un cambio en un solo campo de un issue vuelve a renderizar solo los componentes que leen ese campo, sin volver a renderizar la lista padre ni toda la barra lateral
    • Una actualización de 50 issues implica volver a renderizar 50 celdas, no toda la lista
    • Incluso en un workspace muy activo donde 10 personas editan al mismo tiempo, el costo de recibir actualizaciones crece según los elementos que realmente cambiaron, no según todos los elementos visibles en pantalla
    Publicidad
  • Por qué las tres encajan

    • Si solo hay una base de datos local y no hay escrituras optimistas, al guardar igual aparece un spinner
    • Si solo hay escrituras optimistas y no hay observables granulares, cada actualización sigue causando tirones
    • Si solo hay observables granulares y no hay base de datos local, la carga inicial igual obliga a esperar
    • La velocidad de Linear no es una propiedad de una sola capa, sino de todo el sistema
    • El bundler y el loader shell hacen que el primer paint se sienta rápido, y el motor de sincronización mantiene esa sensación de rapidez incluso después de empezar a usarlo

Diseño para la velocidad

  • La velocidad es un problema de ingeniería, pero también de diseño
  • Si la ruta de acción más rápida exige mouse, tres menús y un clic, el usuario paga ese costo en pasos sin importar la velocidad del motor interno
  • Otro eje de la velocidad de Linear es haber integrado el teclado como herramienta principal tanto para navegar como para completar tareas
  • Todas las tareas comunes tienen shortcut, la command palette se abre con una sola pulsación, y el menú de clic derecho fue construido a medida
  • Todas las acciones tienen shortcut

    • Un solo carácter edita el issue enfocado, las combinaciones de dos letras se usan para navegar y las teclas modifier se usan para acciones globales
    • Desde las primeras etapas de Linear, los shortcut fueron un elemento base, y el motor de sincronización fue diseñado en parte para que cualquier acción pueda ejecutarse en cualquier momento
    • Los shortcut aparecen por toda la UI, y los más usados son de un solo carácter
    • Para no excluir a principiantes, todas las acciones también pueden realizarse con el mouse
  • La command palette siempre está a una tecla de distancia

    • ⌘ k abre una command palette desde la que se puede buscar casi cualquier acción de Linear
    • Se puede buscar issues, proyectos, label, cambios de estado, navegación, creación de issues, configuración, cambio de tema y más
    • La command palette busca en el pool local de objetos MobX, no en el servidor, así que es muy rápida
    • Toda la app se puede usar desde un solo panel, y navegar, crear issues y cambiar estados se hace por búsqueda
    • La command palette se adapta al contexto de trabajo actual y además enseña las acciones clave y los shortcut de cada vista
    • Una app rápida necesita tanto gran ingeniería como gran diseño: la velocidad de ingeniería hace rápida una interacción individual, y la velocidad de diseño acorta la ruta hasta esa interacción
    • En una herramienta que se usa todo el día, la diferencia entre un shortcut y una ruta de mouse de 2 segundos se acumula en cada acción

Animación

  • Una mala animación puede desperdiciar al final los milisegundos que se ganaron optimizando la carga inicial, las actualizaciones y las consultas a la base de datos
  • Un elemento como una animación de height de 500 ms puede arruinar el esfuerzo por evitar que el usuario tenga que esperar
  • Solo hay unas pocas propiedades que conviene animar

    • Los cambios de propiedades en el navegador tienen tres niveles de costo según en qué parte del pipeline de renderizado ocurran
    • Las propiedades compuestas, como transform y opacity, delegan el trabajo a la GPU y se ejecutan de forma independiente del main thread
    • Las propiedades que disparan paint, como color, background-color, border-color y fill, omiten el layout pero provocan un redibujado de píxeles
    • Las propiedades que disparan layout, como width, height, top, left, margin y padding, hacen que se recalcule la posición de todos los elementos posteriores y no deberían animarse
    Publicidad
    /* Enfoque de Linear */
    .row:hover {
      background-color: var(--color-bg-hover);
      transition: background-color 0.12s;
    }
    .icon-arrow {
      transform: translateX(0);
      transition: transform 0.15s;
    }
    
    • Si se anima margin-left, el layout de todas las filas debajo de la fila con hover se recalcula en cada frame durante toda la transición de 200 ms
    • En listas largas de issues, esta diferencia es lo que separa una interfaz fluida de una con tirones
    • La mayoría de las propiedades que Linear anima son propiedades compuestas como transform y opacity, y a veces usa background-color y border-color
  • Hay que saber cuándo contenerse

    • En herramientas de uso diario, las animaciones que lucen bien en un sitio de marketing pueden estorbar el trabajo
    • Incluso un pequeño delay de hover mal ubicado puede ser algo que el usuario note
    • Muchas animaciones de Linear funcionan bien porque hacen referencia a su origen
    • El popover de estado se expande desde la píldora de estado, y el panel del agente se desliza hacia adentro desde el toggle
    • Ese movimiento no es un fade decorativo, sino que cumple una función espacial al indicar de dónde salió el nuevo elemento
  • Mantener duraciones cortas e inmediatas

    --speed-highlightFadeIn: 0s;
    --speed-highlightFadeOut: .15s;
    --speed-quickTransition: .1s;
    --speed-regularTransition: .25s;
    --speed-slowTransition: .35s;
    
    • Muchos design systems establecen duraciones predeterminadas más largas de lo necesario
    • La duración estándar de Material es 200 ms, y el spring de iOS se acerca a 350 ms
    • Los valores predeterminados de Linear están del lado corto frente a las prácticas de la industria
    • Linear usa timings asimétricos para la entrada y la salida
    • El hover highlight, el popover y el panel del agente aparecen de inmediato cuando se invocan, y al cerrarse hacen fade out durante 150 ms
    • La ventana del agente aparece de inmediato y hace fade out de forma similar a macOS

Cómo logra Linear ser rápido

  • El rendimiento de Linear no proviene de un solo secreto ni de una sola tecnología, sino de la acumulación de cientos de decisiones correctas
  • Gran parte de su enfoque es simple y es el resultado de definir desde temprano una arquitectura adecuada para sus usuarios y mantenerla, sin Next, TanStack ni frameworks llamativos
  • El servidor no actúa como source of truth de la UI, sino como objetivo de sincronización
  • La base de datos está dentro del navegador, y los cambios se aplican primero de forma local antes de reconciliarse en segundo plano
  • La carga inicial envía menos código en más fragmentos, y el service worker hace precache del resto mientras el usuario está en la página de inicio de sesión
  • La autenticación asume la ruta normal basándose en el estado local y verifica después
  • El motor de sincronización hidrata desde IndexedDB a observables de MobX por propiedad, por lo que una actualización de 50 issues se resuelve como el rerender de 50 celdas y no como el rerender de toda la lista
  • El modelo de entrada está pensado primero para teclado, y todas las tareas comunes tienen atajos y una command palette global
  • Las animaciones se mantienen en propiedades amigables con la GPU, y no se animan propiedades que disparan layout
  • La parte difícil, más que la implementación en sí, es mantener durante años una atención obsesiva a la calidad de los detalles mientras el codebase madura, escala y se enfrenta a nuevas restricciones

1 comentarios

 
GN⁺ 4 시간 전
Comentarios en Hacker News
  • Si quieres incorporar una experiencia así en una aplicación, vale la pena ver Zero(https://zero.rocicorp.dev/)
    Demo en vivo: https://gigabugs.rocicorp.dev/
    También hay una lista de alternativas aquí: https://zero.rocicorp.dev/docs/when-to-use#alternatives
    Si te da curiosidad cómo funciona por dentro, también vale la pena revisar la documentación de diseño de Replicache: https://doc.replicache.dev/concepts/how-it-works
    Replicache es el predecesor de Zero, y el protocolo central sigue funcionando básicamente de la misma manera

    • Zero es totalmente recomendable. La abstracción es excelente y es un software hecho con mucho cuidado
      Además de la clara ventaja de rendimiento que viene de tener los datos sincronizados en el cliente, también me sorprendió lo mucho que se simplifica el código en React. Con un motor de sincronización, la mayor parte del estado del cliente desaparece y puedes pensar la mayor parte del código de los componentes de forma síncrona
    • Llevo un tiempo usando Zero. Al principio intenté construir internamente un motor de sincronización al estilo Linear, pero descubrí Zero
      Probablemente es la opción más cercana a eso que puedes conseguir sin armar un equipo dedicado
    • Como usuario de Zero, es la herramienta ideal cuando quieres una experiencia de usuario en la que la UI se actualice al instante, en cuestión de milisegundos, cuando cambia la base de datos
  • Siempre escuché que Linear era rápido, pero después de usarlo todos los días, el entusiasmo se me fue apagando. La búsqueda es bastante lenta y la UI a menudo se siente torpe; se ve bien, pero “Pulse” parece una avalancha de ruido incluso a pequeña escala
    Es difícil encontrar lo que necesitas, así que al final termino poniendo todo en favoritos. El Trello de los primeros tiempos era, por mucho, la mejor experiencia para seguimiento de proyectos

    • De verdad odio cuando, en medio de una reunión o un huddle, intento abrir un ticket y me quedo mirando incómodamente una eternidad mientras espera alguna operación, no sé si de carga o de caché
  • El año pasado alguien hizo ingeniería inversa del motor de sincronización de Linear, lo subió a GitHub y además escribió una explicación muy buena
    https://github.com/wzhudev/reverse-linear-sync-engine/blob/m...

  • Estas apps web con sincronización local-first son realmente interesantes y pueden ser muy útiles, pero creo que la premisa está algo equivocada
    La premisa es algo como: “actualizar un issue en Linear toma unos pocos milisegundos; una app CRUD tradicional tarda unos 300 ms en hacer lo mismo”, o “todos los datos que van y vienen entre cliente y servidor pagan un costo de cientos de milisegundos”
    No se puede resolver el problema de que el tiempo de ida y vuelta entre un cliente HTTP y el servidor aumente por la velocidad de la luz, pero sí puedes poner el backend cerca del usuario y hacerlo rápido
    Por ejemplo, es totalmente posible operar un backend de app web que esté a unos 10 ms de RTT para la mayoría de los usuarios, y lograr que el backend también renderice la respuesta en unos 10 ms. Es decir, una app CRUD tradicional también podría hacer esa misma operación no en 300 ms sino en unos 30 ms

    • Me parecía raro ver que 300 ms se describieran como rápido; hasta donde recuerdo, TTFB de 30 ms fue la meta durante mucho tiempo
      Puede que Linear necesite apoyo del frontend porque por razones válidas su backend tarda más, pero eso no se puede generalizar. Cada pedazo de JavaScript también tiene su propio costo
    • Suena extraño decir que “es posible operar un backend de app web a unos 10 ms de RTT para la mayoría de los usuarios”. Desde us-east-1, la única región de AWS con menos de 10 ms es básicamente us-east-2: https://www.cloudping.co/
      us-west-1 está a 60 ms, eu-centra-1 a 100 ms y Asia a 200 ms. Y eso es tráfico entre centros de datos; en el internet público real, hasta una conexión residencial, la latencia es mucho peor
      La base de datos tiene que estar exactamente en una sola región. La pongas donde la pongas, la mayoría de los usuarios del planeta van a estar a más de 100 ms de ahí
      Da igual dónde esté el endpoint, porque para leer y escribir datos el endpoint tiene que comunicarse con la base de datos. En el momento en que intentas replicar los datos cerca del usuario, terminas teniendo una base de datos de sincronización local-first
      Ya sea que la construyas tú o uses una existente, esa base de datos replicada termina teniendo todos los mismos problemas que la sincronización del lado del cliente, y además sigue quedando una latencia de red considerable. No se puede escapar de la física, así que para la mayoría de los usuarios solo queda dar commits de 0.25 segundos, o elegir consistencia eventual, es decir, sincronización
    • Eso solo es posible si los usuarios están bastante cerca unos de otros o, tristemente pero como suele pasar, si basta con que los usuarios de EE. UU. sean rápidos y no te importa el resto
      Claro, puedes poner un “backend intermedio” en algo como una red global de edge de CDN, pero en ese punto acabas pagando el mismo costo de complejidad que con este enfoque de poner el “backend intermedio” en el cliente
    • Puedes poner un backend intermedio en el cliente, escribir los cambios aún no procesados en almacenamiento local y dejar que un worker en segundo plano los envíe al backend con los reintentos necesarios
      En el peor de los casos, el worker en segundo plano emite un mensaje de fallo de actualización y el hilo de UI lo recibe y lo muestra. La ruta de éxito sigue siendo rapidísima
    • ¿Eso también es posible cuando todas estas capas de backend en el edge tienen que compartir una misma base de datos?
  • Es difícil construir una base de datos de consistencia eventual, y quizá esté bien para el caso de uso de Linear, pero es un problema no saber si mi actualización llegó al servidor, es decir, al equipo
    En otros proyectos en los que participé antes, los retrasos de sincronización causaron incontables problemas, así que siempre elijo una solución síncrona. Solo sacaría funciones llamativas cuando de verdad hagan falta, y preferiría optimizar muchísimo el servidor y dejar que el usuario tolere la latencia de red

    • He sufrido inconsistencias en Linear, pero Jira es un basurero, así que ni modo
  • En la empresa usamos Linear. Sé que soy minoría, pero la experiencia de usuario es realmente dura. Incluso cuesta llamarlo rápido
    La página en sí carga técnicamente más o menos rápido, pero la mitad del tiempo terminas viendo cómo cambian los números de la página sin ningún indicador visual de que los datos siguen cargando

    • Un problema que me pasa seguido en Linear es que las escrituras repetidas a veces se sobrescriben entre sí. Escribo, me detengo un momento para pensar, escribo más, y Linear revierte los datos al estado de la primera entrada
      Es tan malo que en Linear solo creo issues con una descripción de una sola frase, y luego me voy a GitHub para completar los detalles. Eso es exactamente lo único que Linear hace bien y rápido
    • Linear se convirtió en lo que intentaba eliminar: una herramienta compleja
      Es una lástima, pero si la empresa quiere sobrevivir y subir al mercado de gama alta, en la práctica no hay otro camino
    • No uso Chrome, así que no sé si será por eso, pero las páginas de Linear se congelan seguido o tardan varios segundos en la primera carga
      Yo no usaría la palabra “rápido”. Si de entrada tarda 30 segundos en cargar, no importa mucho que la actualización de un issue baje de 300 ms a “unos cuantos” milisegundos
    • En un trabajo anterior cambiamos de Linear a Jira por su UX rarísimo. Había muchos íconos cuyo significado era difícil de entender, poca capacidad de descubrimiento y casi ninguna indicación de por qué cambiaba el contenido de la página
    • Es realmente terrible. Tuve que preguntarle a un compañero cómo agregar una fecha límite a un elemento, y estaba escondido dentro del panel de navegación
      Es mejor que Jira, pero esa barra está muy baja
  • Qué genial. Quizá podría meter algo parecido en el juego de navegador y el motor que estoy desarrollando, y así eliminar por completo el estado de carga después de la carga inicial. Lo mío es una arquitectura de assets estáticos totalmente del lado del cliente, sin servidor
    He estado obsesionado con el rendimiento de este juego. Antes del fin de semana pasado estaba batallando para mantener 120 fps en una M1 MacBook Pro simulando 128 jugadores concurrentes, con pathfinding, lógica estratégica pesada y renderizado, todo dentro del viewport, con caídas de frames muy ocasionales y un frame time de unos 4 ms
    Durante el fin de semana me puse a trabajar duro en rendimiento y ahora puedo simular 2048 jugadores concurrentes con un frame time de menos de un milisegundo. Esa cifra incluye renderizado, toda la lógica y hasta generación procedural
    Además, al aplicar un throttling de CPU de 11.2x para simular un dispositivo móvil de gama baja, sigo obteniendo 60 fps estables con un frame time de unos 5 ms en 256~512 jugadores concurrentes. Ahora mismo los cuellos de botella principales son algunos problemas de lógica y el tiempo de inicio/arranque que todavía debo mejorar en dispositivos de gama baja, y creo que podría aprender algo de Linear

  • Siempre sentí que Linear en realidad es bastante lento. Hubo semanas en las que, si dejaba una pestaña abierta por un tiempo, se iba a 100% de CPU

    • En Firefox también parece usar mucha memoria. No puedo tener varias pestañas de Linear abiertas al mismo tiempo
  • Está interesante. La verdad, nunca he pensado en Linear como algo “rápido”. Como la mayoría de las web apps, se siente con latencia, aunque comparado con JIRA obviamente es velocidad luz
    Linear en sí es excelente y, después de la tortura de JIRA, de verdad se siente refrescante. Si vamos a hablar de rutas optimistas y de “rapidez”, quizá primero habría que hablar de Gmail

  • La respuesta a la velocidad es la precarga. Básicamente, se descarga una base de datos cliente en el momento de la inicialización y se aplica una estrategia de invalidación de caché
    Hice starfx para abordar el aspecto de sincronización de datos de este paradigma: https://starfx.bower.sh/learn#data-loading-strategy-stale-wh...