Llevamos service workers a la Búsqueda de Google

La historia de qué se envió, cómo se midió el impacto y las concesiones que se realizaron.

Información general

Si buscas casi cualquier tema en Google, se te mostrará una página reconocible al instante de resultados significativos y relevantes. Lo que probablemente no te diste cuenta es que esta página de resultados de búsqueda, en ciertas situaciones, se entrega mediante una parte potente de la tecnología web llamada service worker.

El lanzamiento de la compatibilidad con los service workers para la Búsqueda de Google sin afectar negativamente el rendimiento requirió decenas de ingenieros que trabajaran en varios equipos. Esta es la historia de qué se envió, cómo se midió el rendimiento y qué compensaciones se realizaron.

Motivos clave para explorar los service workers

Para agregar un service worker a una app web, al igual que cuando haces un cambio arquitectónico en tu sitio, debes tener en cuenta un conjunto claro de objetivos. Para el equipo de la Búsqueda de Google, había algunas razones clave por las que valía la pena explorar la posibilidad de agregar un service worker.

Almacenamiento en caché limitado de los resultados de la búsqueda

El equipo de la Búsqueda de Google descubrió que es común que los usuarios busquen los mismos términos más de una vez en un período corto. En lugar de activar una nueva solicitud de backend solo para obtener los mismos resultados, el equipo de Búsqueda quería aprovechar el almacenamiento en caché y entregar esas solicitudes repetidas de forma local.

No se puede descartar la importancia de la actualidad y, a veces, los usuarios buscan los mismos términos de manera reiterada porque es un tema en evolución y esperan ver resultados nuevos. El uso de un service worker permite que el equipo de Búsqueda implemente una lógica detallada para controlar el ciclo de vida de los resultados de búsqueda almacenados en caché local y lograr el equilibrio exacto entre velocidad y actualización que considere mejor a los usuarios.

Experiencia significativa sin conexión

Además, el equipo de la Búsqueda de Google quería ofrecer una experiencia sin conexión significativa. Cuando un usuario desea obtener información sobre un tema, quiere ir directamente a la página de la Búsqueda de Google y comenzar a buscar, sin preocuparse por tener una conexión a Internet activa.

Sin un service worker, al visitar la página de la Búsqueda de Google sin conexión, simplemente se te dirige a la página de error de red estándar del navegador, y los usuarios tendrían que recordar regresar y volver a intentarlo cuando se recupere la conexión. Con un service worker, es posible entregar una respuesta HTML sin conexión personalizada y permitir que los usuarios ingresen su búsqueda de inmediato.

Captura de pantalla de la interfaz de reintento en segundo plano.

Los resultados no estarán disponibles hasta que haya una conexión a Internet, pero el service worker permite que la búsqueda se aplace y se envíe a los servidores de Google cuando el dispositivo se vuelva a conectar con la API de sincronización en segundo plano.

Almacenamiento y entrega de JavaScript más inteligentes

Otra motivación fue optimizar el almacenamiento en caché y la carga del código JavaScript modularizado que impulsa los distintos tipos de funciones en la página de resultados de búsqueda. El empaquetado de JavaScript ofrece varios beneficios que tienen sentido cuando no hay participación de service worker, por lo que el equipo de Búsqueda no quería simplemente dejar de agruparse por completo.

El equipo de Búsqueda usó la capacidad de un service worker para crear versiones de fragmentos detallados de JavaScript y almacenarlos en caché durante el tiempo de ejecución y, así, sospechar que podía reducir la cantidad de pérdida de caché y garantizar que el código JavaScript que se reutiliza en el futuro se pudiera almacenar en caché de manera eficiente. La lógica de cada service worker puede analizar una solicitud HTTP saliente en busca de un paquete que contenga varios módulos de JavaScript y entregarla combinando varios módulos almacenados en caché local. De esta forma, se "desagrupa" de manera efectiva cuando sea posible. Esto ahorra ancho de banda del usuario y mejora la capacidad de respuesta general.

También hay beneficios de rendimiento de usar JavaScript almacenado en caché que entrega un service worker: en Chrome, una representación de código de bytes analizada de ese JavaScript se almacena y reutiliza, lo que genera menos trabajo que se debe realizar en el tiempo de ejecución para ejecutar el JavaScript en la página.

Desafíos y soluciones

Estos son algunos de los obstáculos que se deben superar para alcanzar los objetivos declarados por el equipo. Si bien algunos de estos desafíos son específicos de la Búsqueda de Google, muchos de ellos se aplican a una amplia variedad de sitios que podrían estar considerando la implementación de un service worker.

Problema: sobrecarga del service worker

El mayor desafío, y el verdadero bloqueador para iniciar un service worker en la Búsqueda de Google, era asegurarse de que no hiciera nada que pudiera aumentar la latencia percibida por el usuario. La Búsqueda de Google se toma el rendimiento muy en serio y, en el pasado, bloqueó los lanzamientos de funciones nuevas si contribuía incluso decenas de milisegundos de latencia adicional para una población de usuarios determinada.

Cuando el equipo comenzó a recopilar datos de rendimiento durante sus primeros experimentos, fue evidente que habría un problema. El código HTML que se muestra en respuesta a las solicitudes de navegación para la página de resultados de búsqueda es dinámico y varía considerablemente según la lógica que necesite ejecutarse en los servidores web de la Búsqueda. Actualmente, no hay forma de que el service worker replique esta lógica y muestre el HTML almacenado en caché de inmediato. Lo mejor que podría hacer es pasar las solicitudes de navegación a los servidores web de backend, lo que requiere una solicitud de red.

Sin un service worker, esta solicitud de red ocurre inmediatamente después de que el usuario navega. Cuando se registra un service worker, siempre se debe iniciar y tener la oportunidad de ejecutar sus controladores de eventos fetch, incluso cuando no hay posibilidades de que esos controladores de recuperación hagan algo más que ir a la red. La cantidad de tiempo que lleva iniciar y ejecutar el código del service worker es pura sobrecarga que se agrega a cada navegación.

Ilustración del inicio del SW que bloquea la solicitud de navegación.

Esto pone a la implementación de service worker en una desventaja de latencia demasiado alta para justificar cualquier otro beneficio. Además, el equipo descubrió que, según la medición de los tiempos de inicio del service worker en dispositivos reales, se distribuía una amplia distribución de los tiempos de inicio, y algunos dispositivos móviles de gama baja tardaban casi todo el tiempo en iniciar el service worker que el tiempo que tomaría hacer la solicitud de red del código HTML de la página de resultados.

Solución: Usa la precarga de navegación

La función más importante y única que permitió al equipo de la Búsqueda de Google avanzar con el lanzamiento de su service worker es la precarga de la navegación. El uso de la precarga de navegación es una mejora clave del rendimiento para cualquier service worker que necesite usar una respuesta de la red a fin de satisfacer las solicitudes de navegación. Proporciona una sugerencia al navegador para que comience a realizar la solicitud de navegación de inmediato, al mismo tiempo que se inicia el service worker:

Una ilustración del inicio de SW realizada en paralelo con la solicitud de navegación.

Siempre que la cantidad de tiempo que tarda el service worker en iniciarse es menor que la cantidad de tiempo que se demora en obtener una respuesta de la red, no debería haber ninguna sobrecarga de latencia presentada por el service worker.

El equipo de Búsqueda también debía evitar el uso de un service worker en dispositivos móviles de gama baja en los que el tiempo de inicio del service worker podía superar la solicitud de navegación. Como no hay una regla estricta para lo que constituye un dispositivo de "baja gama", idearon la heurística de comprobar la memoria RAM total instalada en el dispositivo. Cualquier elemento que tenga menos de 2 gigabytes de memoria pertenece a la categoría de dispositivo de gama baja, en la que el tiempo de inicio del service worker sería inaceptable.

El espacio de almacenamiento disponible es otra consideración, ya que el conjunto completo de recursos que se almacenará en caché para uso futuro puede ejecutarse en varios megabytes. La interfaz navigator.storage permite que la página de la Búsqueda de Google determine con anticipación si sus intentos de almacenar datos en caché corren el riesgo de fallar debido a fallas en la cuota de almacenamiento.

Esto dejaba al equipo de Búsqueda con varios criterios que podrían usar para determinar si usar o no un service worker: si un usuario llega a la página de la Búsqueda de Google con un navegador que admita la precarga de navegación y tiene al menos 2 gigabytes de RAM y suficiente espacio de almacenamiento libre, se registra un service worker. Los navegadores o dispositivos que no cumplan con esos criterios no terminarán con un service worker, pero verán la misma experiencia de la Búsqueda de Google que siempre.

Un beneficio secundario de este registro selectivo es la capacidad de enviar un service worker más pequeño y eficiente. Orientar a navegadores bastante modernos para ejecutar el código de service worker elimina la sobrecarga de transpilación y polyfills en los navegadores más antiguos. Esto redujo alrededor de 8 kilobytes de código JavaScript sin comprimir del tamaño total de la implementación del service worker.

Problema: Permisos de service worker

Una vez que el equipo de Búsqueda realizó suficientes experimentos de latencia y estuvo seguro de que el uso de la precarga de navegación les ofrecía una ruta viable y neutra en términos de latencia para utilizar un service worker, se empezaron a pasar algunos problemas prácticos. Uno de esos problemas está relacionado con las reglas de alcance de los service worker. El alcance de un service worker determina qué páginas puede controlar potencialmente.

El alcance funciona según el prefijo de la ruta de URL. En el caso de los dominios que alojan una sola app web, esto no es un problema, ya que normalmente solo se usa un service worker con el alcance máximo de /, que podría controlar cualquier página del dominio. Sin embargo, la estructura de las URLs de la Búsqueda de Google es un poco más complicada.

Si al service worker se le diera el alcance máximo de /, podría controlar cualquier página alojada en www.google.com (o el equivalente regional), y allí hay URLs dentro de ese dominio que no tienen nada que ver con la Búsqueda de Google. Un alcance más razonable y restrictivo sería /search, que al menos eliminaría las URLs completamente no relacionadas con los resultados de la búsqueda.

Lamentablemente, incluso esa ruta de URL /search se comparte entre diferentes variantes de resultados de la Búsqueda de Google, con parámetros de consulta de URL que determinan qué tipo específico de resultado de la búsqueda se muestra. Algunas de esas variantes usan bases de código completamente diferentes a las de la página de resultados de búsqueda web tradicional. Por ejemplo, Image Search y Shopping Search se entregan a la ruta de URL /search con diferentes parámetros de consulta, pero ninguna de esas interfaces estaba lista para ofrecer su propia experiencia de service worker (todavía).

Solución: Crea un framework de envío y enrutamiento

Si bien existen algunas propuestas que permiten algo más potente que los prefijos de ruta de URL para determinar los alcances de los service workers, el equipo de la Búsqueda de Google se quedó con la implementación de un service worker que no hacía nada en un subconjunto de páginas que controlaba.

Para solucionar este problema, el equipo de la Búsqueda de Google creó un framework de envío y enrutamiento personalizado que se podía configurar para buscar criterios como los parámetros de búsqueda de la página del cliente y usarlos para determinar qué ruta de código específica debe eliminarse. En lugar de codificar las reglas, el sistema se creó para ser flexible y permitir a los equipos que comparten el espacio de URL, como la búsqueda de imágenes y Shopping, incorporar su propia lógica de service worker más adelante, si deciden implementarla.

Problema: Resultados y métricas personalizados

Los usuarios pueden acceder a la Búsqueda de Google con sus Cuentas de Google, y la experiencia con los resultados de la búsqueda se puede personalizar según los datos específicos de su cuenta. Los usuarios que accedieron a sus cuentas se identifican mediante cookies de navegador específicas, que es un estándar respetado y ampliamente compatible.

Sin embargo, una desventaja de usar cookies del navegador es que no se exponen dentro de un service worker y no hay forma de examinar automáticamente sus valores y asegurarse de que no hayan cambiado debido a que un usuario sale de la cuenta o cambia de cuenta. (Hay un esfuerzo en curso para brindar acceso de cookies a los service workers, pero, hasta el momento de la redacción de este documento, el enfoque es experimental y no es ampliamente compatible).

Una discrepancia entre la vista del service worker del usuario actual que accedió y el usuario real que accedió a la interfaz web de la Búsqueda de Google podría generar resultados de la búsqueda personalizados de forma incorrecta o métricas y registros atribuidos incorrectamente. Cualquiera de esas situaciones de falla sería un problema grave para el equipo de la Búsqueda de Google.

Solución: Envía cookies con postMessage

En lugar de esperar a que se inicien las APIs experimentales y proporcionen acceso directo a las cookies del navegador dentro de un service worker, el equipo de la Búsqueda de Google implementó una solución provisional: cada vez que se carga una página controlada por el service worker, la página lee las cookies relevantes y usa postMessage() para enviarlas al service worker.

Luego, el service worker compara el valor actual de la cookie con el valor esperado y, si hay una discrepancia, borra definitivamente los datos específicos del usuario de su almacenamiento y vuelve a cargar la página de resultados de búsqueda sin ninguna personalización incorrecta.

Los pasos específicos que realiza el service worker para restablecer elementos a un modelo de referencia son específicos de los requisitos de la Búsqueda de Google. Sin embargo, el mismo enfoque general puede resultar útil para otros desarrolladores que trabajan con datos personalizados almacenados en las cookies de los navegadores.

Problema: experimentos y dinamismo

Como se mencionó anteriormente, el equipo de la Búsqueda de Google depende en gran medida de la ejecución de experimentos en producción y de probar los efectos del código y las funciones nuevos en el mundo real antes de activarlos de forma predeterminada. Esto puede ser un poco desafiante con un service worker estático que depende en gran medida de los datos almacenados en caché, ya que habilitar o inhabilitar la participación de los usuarios en los experimentos suele requerir comunicación con el servidor de backend.

Solución: secuencia de comandos del service worker generada de forma dinámica

La solución que el equipo tomó fue usar una secuencia de comandos de service worker generada de forma dinámica, personalizada por el servidor web para cada usuario individual, en lugar de una secuencia de comandos de service worker única y estática que se genera con anticipación. La información sobre los experimentos que podrían afectar el comportamiento del service worker o las solicitudes de red en general se incluye directamente en estas secuencias de comandos personalizadas del service worker. El cambio de conjuntos de experiencias activas para un usuario se realiza a través de una combinación de técnicas tradicionales, como las cookies del navegador, y se entrega código actualizado en la URL del service worker registrada.

El uso de una secuencia de comandos de service worker generada de forma dinámica también facilita la tarea de proporcionar una solución de emergencia en el caso improbable de que la implementación de un service worker tenga un error irrecuperable que se debe evitar. La respuesta dinámica del trabajador del servidor podría ser una implementación no operativa que inhabilite eficazmente el service worker para algunos o todos los usuarios actuales.

Problema: Se están coordinando las actualizaciones

Uno de los desafíos más difíciles a los que se enfrenta cualquier implementación de service worker real es establecer una compensación razonable entre evitar la red y la caché y, al mismo tiempo, garantizar que los usuarios existentes obtengan actualizaciones y cambios críticos poco después de que se implementen en producción. El equilibrio adecuado depende de muchos factores:

  • Si tu app web es una app de una sola página de larga duración que el usuario mantiene abierta de forma indefinida, sin tener que navegar a páginas nuevas.
  • La cadencia de implementación para las actualizaciones de tu servidor web de backend
  • Si el usuario promedio toleraría el uso de una versión un poco desactualizada de tu app web o si la actualización es la prioridad principal

Mientras experimentaba con service workers, el equipo de la Búsqueda de Google se aseguró de mantener los experimentos en ejecución en una serie de actualizaciones de backend programadas para garantizar que las métricas y la experiencia del usuario coincidieran mejor con lo que los usuarios regresarían en el mundo real.

Solución: Equilibrar la actualidad y el uso de la caché

Después de probar varias opciones de configuración diferentes, el equipo de la Búsqueda de Google descubrió que la siguiente configuración proporcionaba el equilibrio adecuado entre la actualización y el uso de la caché.

La URL de la secuencia de comandos del service worker se entrega con el encabezado de respuesta Cache-Control: private, max-age=1500 (1,500 segundos o 25 minutos) y se registra con updateViaCache configurado como "all" para garantizar que se respete el encabezado. Como puedes imaginar, el backend web de la Búsqueda de Google es un gran conjunto de servidores distribuidos a nivel global que requiere un tiempo de actividad lo más cercano posible. La implementación de un cambio que afectaría el contenido de la secuencia de comandos del service worker se realiza de forma progresiva.

Si un usuario accede a un backend que se actualizó y, luego, navega con rapidez a otra página que encuentra un backend que aún no recibió el service worker actualizado, terminaría cambiando de versión varias veces. Por lo tanto, decirle al navegador que solo se moleste en comprobar si hay una secuencia de comandos actualizada si pasaron 25 minutos desde la última verificación no tiene un problema importante. La ventaja de habilitar este comportamiento es que se reduce significativamente el tráfico que recibe el extremo que genera de forma dinámica la secuencia de comandos del service worker.

Además, se configura un encabezado ETag en la respuesta HTTP de la secuencia de comandos del service worker, lo que garantiza que, cuando se realice una verificación de actualización después de 25 minutos, el servidor pueda responder de manera eficiente con una respuesta HTTP 304 si no se han realizado actualizaciones en el service worker implementado durante el tiempo.

Si bien algunas interacciones dentro de la app web de la Búsqueda de Google usan navegaciones de una sola página con el estilo de la app (es decir, a través de la API de History), en su mayor parte, la Búsqueda de Google es una app web tradicional que usa navegaciones "reales". Esto entra en juego cuando el equipo decidió que sería eficaz usar dos opciones que aceleran el ciclo de vida de la actualización del service worker: clients.claim() y skipWaiting(). Cuando haces clic en la interfaz de la Búsqueda de Google, sueles navegar a documentos HTML nuevos. Llamar a skipWaiting garantiza que un service worker actualizado pueda controlar esas solicitudes de navegación nuevas inmediatamente después de la instalación. De manera similar, si se llama a clients.claim(), el service worker actualizado puede comenzar a controlar cualquier página abierta de la Búsqueda de Google que no esté controlada después de la activación del service worker.

El enfoque que adoptó la Búsqueda de Google no es necesariamente una solución que funcione para todos, sino que fue el resultado de realizar pruebas A/B cuidadosamente de varias combinaciones de opciones de entrega hasta que encontraron la que funcionaba mejor. Los desarrolladores cuya infraestructura de backend les permita implementar actualizaciones con mayor rapidez pueden preferir que el navegador busque una secuencia de comandos de service worker actualizada con la mayor frecuencia posible, ya que ignora siempre la caché HTTP. Si compilas una app de una sola página que los usuarios puedan mantener abierta durante un período prolongado, es probable que usar skipWaiting() no sea la opción adecuada para ti, ya que corres el riesgo de encontrar incoherencias de caché si permites que el service worker nuevo se active mientras hay clientes de larga duración.

Conclusiones principales

De forma predeterminada, los service workers no tienen neutralidad en el rendimiento

Agregar un service worker a tu app web implica insertar un fragmento adicional de JavaScript que se debe cargar y ejecutar antes de que la app web obtenga respuestas a sus solicitudes. Si esas respuestas provienen de una caché local en lugar de la red, la sobrecarga de ejecutar el service worker suele ser insignificante en comparación con la mejora de rendimiento que se obtiene al pasar a la caché primero. Sin embargo, si sabes que tu service worker siempre tiene que consultar la red cuando se manejan solicitudes de navegación, usar la precarga de navegación es una mejora fundamental para el rendimiento.

Los service workers son (todavía) una mejora progresiva

La historia de asistencia de un service worker es mucho más interesante hoy en día que hace un año. Todos los navegadores actualizados ahora cuentan con al menos cierta compatibilidad con service worker, pero, lamentablemente, hay algunas funciones avanzadas de service worker, como la sincronización en segundo plano y la carga previa de navegación, que no se lanzan de forma universal. La verificación de atributos para el subconjunto específico de funciones que sabes que necesitas y el registro de un service worker solo cuando están presentes es un enfoque razonable.

De manera similar, si ejecutaste experimentos en el entorno y sabes que los dispositivos de gama baja tienen un rendimiento deficiente debido a la sobrecarga adicional de un service worker, también puedes abstenerse de registrar un service worker en esas situaciones.

Debes seguir tratando los service worker como una mejora progresiva que se agrega a una app web cuando se cumplen todos los requisitos previos y el service worker agrega algo positivo a la experiencia del usuario y al rendimiento de carga general.

Medir todo

La única forma de averiguar si el envío de un service worker tuvo un impacto positivo o negativo en las experiencias de los usuarios es experimentar y medir los resultados.

Los detalles de la configuración de mediciones significativas dependen del proveedor de estadísticas que uses y de cómo realizas experimentos en la configuración de la implementación. En este caso de éxito, se detalla un enfoque en el que se usa Google Analytics para recopilar métricas y se basa en la experiencia con service workers en la app web de Google I/O.

No objetivos

Si bien muchos miembros de la comunidad de desarrollo web asocian service workers con apps web progresivas, compilar una "AWP de la Búsqueda de Google" no fue un objetivo inicial del equipo. Actualmente, la app web de la Búsqueda de Google no proporciona metadatos a través de un manifiesto de app web ni incentiva a los usuarios a completar el flujo de Agregar a la pantalla principal. En la actualidad, el equipo de Búsqueda está satisfecho con los usuarios que llegan a su app web a través de los puntos de entrada tradicionales de la Búsqueda de Google.

En lugar de intentar convertir la experiencia web de la Búsqueda de Google en el equivalente de lo que esperarías de una aplicación instalada, el enfoque en el lanzamiento inicial fue mejorar de manera progresiva el sitio web existente.

Agradecimientos

Agradecemos a todo el equipo de desarrollo web de la Búsqueda de Google por su trabajo en la implementación de service worker y por compartir el material de referencia que se incluyó en la redacción de este artículo. Un agradecimiento en particular es a Philippe Golle, Rajesh Jagannathan, R. Samuel Klatchko, Andy Martone, Leonardo Peña, Rachel Shearer, Greg Terrono y Clay Woolam.

Actualización (octubre de 2021): Desde que se publicó este artículo originalmente, el equipo de la Búsqueda de Google volvió a evaluar los beneficios y las compensaciones de su arquitectura de service worker actual. Se retirará el service worker descrito anteriormente. A medida que evoluciona la infraestructura web de la Búsqueda de Google, el equipo puede volver a examinar el diseño de su service worker.