Llevamos service workers a la Búsqueda de Google

La historia de lo que se lanzó, cómo se midió el impacto y las compensaciones que se hicieron.

Publicado el 20 de junio de 2019

Busca casi cualquier tema en Google y verás una página reconocible al instante con resultados significativos y relevantes. Lo que probablemente no sabías es que, en ciertas situaciones, esta página de resultados de la búsqueda se publica con una potente tecnología web llamada service worker.

El lanzamiento de la compatibilidad con Service Worker para la Búsqueda de Google sin afectar negativamente el rendimiento requirió que decenas de ingenieros trabajaran en varios equipos. Esta es la historia de lo que se lanzó, cómo se midió el rendimiento y qué compensaciones se hicieron.

Motivos principales para explorar los Service Workers

Agregar un service worker a una app web, al igual que realizar cualquier cambio arquitectónico en tu sitio, debe hacerse con un conjunto claro de objetivos en mente. Para el equipo de la Búsqueda de Google, había algunos motivos clave por los 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 satisfacer esas solicitudes repetidas de forma local.

No se puede subestimar la importancia de la actualidad, y, a veces, los usuarios buscan los mismos términos varias veces porque es un tema en evolución y esperan ver resultados actualizados. El uso de un trabajador de servicio permite que el equipo de Búsqueda implemente una lógica detallada para controlar la vida útil de los resultados de búsqueda almacenados en caché de forma local y lograr el equilibrio exacto entre velocidad y actualidad que considera que es el mejor para los usuarios.

Experiencia sin conexión significativa

Además, el equipo de la Búsqueda de Google quería proporcionar una experiencia sin conexión significativa. Cuando un usuario quiere 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, visitar la página de la Búsqueda de Google sin conexión solo llevaría a la página de error de red estándar del navegador, y los usuarios tendrían que recordar volver y volver a intentarlo una vez que se restableciera su 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 posponga y se envíe a los servidores de Google en cuanto el dispositivo vuelva a estar en línea con la API de sincronización en segundo plano.

Almacenamiento en caché 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 la 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 detener el empaquetado por completo.

Al usar la capacidad de un service worker para crear versiones y almacenar en caché fragmentos detallados de JavaScript en el tiempo de ejecución, el equipo de Búsqueda sospechó que podría reducir la cantidad de rotación de la caché y garantizar que el JavaScript que se reutilice en el futuro se pueda almacenar en caché de manera eficiente. La lógica dentro de su Service Worker puede analizar una solicitud HTTP saliente para un paquete que contiene varios módulos de JavaScript y satisfacerla uniendo varios módulos almacenados en caché de forma local, lo que "desempaqueta" de manera efectiva cuando es posible. Esto ahorra ancho de banda del usuario y mejora la capacidad de respuesta general.

También existen beneficios de rendimiento al usar JavaScript almacenado en caché que proporciona un trabajador de servicio: en Chrome, se almacena y se reutiliza una representación analizada en código de bytes de ese JavaScript, 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 debieron superar para alcanzar los objetivos establecidos del 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 considerar la implementación de un service worker.

Problema: Sobrecarga del service worker

El mayor desafío, y el único obstáculo real para lanzar un service worker en la Búsqueda de Google, fue garantizar 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ó el lanzamiento de nuevas funciones si contribuían incluso con 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, se hizo evidente que habría un problema. El código HTML que se devuelve en respuesta a las solicitudes de navegación para la página de resultados de búsqueda es dinámico y varía mucho según la lógica que debe ejecutarse en los servidores web de la Búsqueda. Actualmente, el service worker no puede replicar esta lógica y devolver 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 se realiza inmediatamente después de la navegación del usuario. Cuando se registra un service worker, siempre debe iniciarse y tener la oportunidad de ejecutar sus controladores de eventos fetch, incluso cuando no haya ninguna posibilidad de que esos controladores de recuperación hagan algo más que ir a la red. El tiempo que se tarda en iniciar y ejecutar el código del trabajador de servicio es una sobrecarga pura que se agrega a cada navegación:

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

Esto hace que la implementación del trabajador de servicio tenga una desventaja de latencia demasiado grande como para justificar cualquier otro beneficio. Además, el equipo descubrió que, según las mediciones de los tiempos de inicio de los service workers en dispositivos reales, había una amplia distribución de los tiempos de inicio, y algunos dispositivos móviles de gama baja tardaban casi el mismo tiempo en iniciar el service worker que el que podría tardar en realizar la solicitud de red para el HTML de la página de resultados.

Solución: Usa la precarga de navegación

La función única y más importante que permitió al equipo de la Búsqueda de Google avanzar con el lanzamiento de su Service Worker es la precarga de navegación. Usar la precarga de navegación es una mejora clave del rendimiento para cualquier trabajador de servicio que necesite usar una respuesta de la red para satisfacer 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:

Ilustración del inicio del software realizado en paralelo con la solicitud de navegación.

Siempre y cuando el tiempo que tarda el service worker en iniciarse sea menor que el tiempo que tarda en obtener una respuesta de la red, el service worker no debería introducir ninguna sobrecarga de latencia.

El equipo de Búsqueda también necesitaba evitar el uso de un service worker en dispositivos móviles de gama baja, en los que el tiempo de arranque del service worker podía superar la solicitud de navegación. Como no hay una regla estricta para definir qué constituye un dispositivo "de gama baja", se les ocurrió la heurística de verificar la RAM total instalada en el dispositivo. Todo lo que tuviera menos de 2 gigabytes de memoria entraba en la categoría de dispositivos de gama baja, en la que el tiempo de inicio del service worker sería inaceptable.

El espacio de almacenamiento disponible es otro factor a tener en cuenta, ya que el conjunto completo de recursos que se almacenarán en caché para su uso futuro puede ocupar 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 errores en la cuota de almacenamiento.

Esto dejó al equipo de Búsqueda con varios criterios que podía usar para determinar si debía usar un service worker: si un usuario llega a la página de la Búsqueda de Google con un navegador que admite la precarga de navegación, tiene al menos 2 gigabytes de RAM y suficiente espacio de almacenamiento libre, entonces se registra un service worker. Los navegadores o dispositivos que no cumplan con esos criterios no tendrán un service worker, pero seguirán viendo la misma experiencia de la Búsqueda de Google que siempre tuvieron.

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

Problema: Alcances del Service Worker

Una vez que el equipo de Búsqueda realizó suficientes experimentos de latencia y tuvo la certeza de que la precarga de navegación ofrecía una ruta viable y neutral en cuanto a la latencia para usar un Service Worker, comenzaron a surgir algunos problemas prácticos. Uno de esos problemas se relaciona con las reglas de alcance del 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. Para los dominios que alojan una sola app web, esto no es un problema, ya que normalmente usarías un service worker con el alcance máximo de /, que podría tomar el control de cualquier página del dominio. Sin embargo, la estructura de URL de la Búsqueda de Google es un poco más complicada.

Si el trabajador de servicio tuviera el alcance máximo de /, podría tomar el control de cualquier página alojada en www.google.com (o el equivalente regional), y hay URLs en 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 que no están relacionadas con los resultados de la búsqueda.

Lamentablemente, incluso esa ruta de URL de /search se comparte entre diferentes versiones de los resultados de la Búsqueda de Google, y los parámetros de búsqueda de la URL determinan qué tipo específico de resultado de la búsqueda se muestra. Algunas de esas versiones usan bases de código completamente diferentes a las de la página de resultados de la búsqueda web tradicional. Por ejemplo, la Búsqueda de imágenes y la Búsqueda de compras se publican en la ruta de URL /search con diferentes parámetros de búsqueda, pero ninguna de esas interfaces estaba lista para lanzar su propia experiencia de Service Worker (todavía).

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

Si bien existen algunas propuestas que permiten algo más potente que los prefijos de rutas de URL para determinar los alcances de los Service Workers, el equipo de la Búsqueda de Google no podía implementar un Service Worker que no hiciera nada para un subconjunto de páginas que controlaba.

Para solucionar este problema, el equipo de la Búsqueda de Google creó un marco de trabajo de enrutamiento y envío personalizado que se podía configurar para verificar 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 seguir. En lugar de codificar reglas de forma rígida, el sistema se diseñó para ser flexible y permitir que los equipos que comparten el espacio de URL, como la Búsqueda de imágenes y la Búsqueda de compras, incorporen 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 su experiencia de resultados de la búsqueda se puede personalizar en función de los datos de su cuenta en particular. Los usuarios que accedieron a su cuenta se identifican con cookies del navegador específicas, que son un estándar venerable y ampliamente compatible.

Sin embargo, una desventaja de usar cookies del navegador es que no se exponen dentro de un trabajador de servicio y no hay forma de examinar automáticamente sus valores y garantizar que no hayan cambiado debido a que un usuario cerró la sesión o cambió de cuenta. (Se está trabajando para incorporar el acceso a las cookies en los service workers, pero, en el momento de escribir este artículo, el enfoque es experimental y no se admite de forma generalizada).

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

Solución: Envía cookies con postMessage

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

Luego, el trabajador de servicio verifica el valor actual de la cookie con el valor que espera y, si hay una discrepancia, toma medidas para borrar los datos específicos del usuario de su almacenamiento y vuelve a cargar la página de resultados de la búsqueda sin ninguna personalización incorrecta.

Los pasos específicos que sigue el trabajador de servicio para restablecer la configuración a un nivel básico son particulares de los requisitos de la Búsqueda de Google, pero el mismo enfoque general puede ser útil para otros desarrolladores que trabajan con datos personalizados indexados a partir de las cookies del navegador.

Problema: Experimentos y dinamismo

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

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

La solución que eligió el equipo fue usar una secuencia de comandos de Service Worker generada de forma dinámica y personalizada por el servidor web para cada usuario individual, en lugar de una sola secuencia de comandos de Service Worker 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 estos secuencias de comandos de service worker personalizados. El cambio de los 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 la publicación de código actualizado en la URL del trabajador de servicio registrado.

Usar una secuencia de comandos del service worker generada de forma dinámica también facilita proporcionar una solución de escape en el improbable caso de que una implementación del service worker tenga un error fatal que se deba evitar. La respuesta dinámica del trabajador del servicio podría ser una implementación no operativa, lo que inhabilitaría de manera efectiva el trabajador del servicio para algunos o todos los usuarios actuales.

Problema: Coordinación de actualizaciones

Uno de los desafíos más difíciles que enfrenta cualquier implementación de Service Worker en el mundo real es idear una compensación razonable entre evitar la red en favor de la caché y, al mismo tiempo, garantizar que los usuarios existentes reciban 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 un usuario mantiene abierta de forma indefinida, sin navegar a páginas nuevas
  • Cadencia de implementación de las actualizaciones del servidor web de backend.
  • Si el usuario promedio toleraría usar una versión ligeramente desactualizada de tu app web o si la actualidad es la prioridad principal

Mientras experimentaba con los service workers, el equipo de la Búsqueda de Google se aseguró de que los experimentos se ejecutaran en varias actualizaciones programadas del backend para garantizar que las métricas y la experiencia del usuario coincidieran más con lo que los usuarios que regresan verían en el mundo real.

Solución: Equilibra la actualización y el uso de la caché

Después de probar varias opciones de configuración diferentes, el equipo de 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 publica con el encabezado de respuesta Cache-Control: private, max-age=1500 (1,500 segundos o 25 minutos) y se registra con updateViaCache establecido en "all" para garantizar que se respete el encabezado. Como puedes imaginar, el backend web de la Búsqueda de Google es un conjunto grande de servidores distribuidos a nivel global que requiere un tiempo de actividad cercano al 100%. 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 rápidamente a otra página que accede a un backend que aún no recibió el service worker actualizado, terminará alternando entre versiones varias veces. Por lo tanto, indicarle al navegador que solo se moleste en verificar si hay un script actualizado si pasaron 25 minutos desde la última verificación no tiene una desventaja significativa. 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 establece un encabezado ETag en la respuesta HTTP del script del trabajador de servicio, lo que garantiza que, cuando se realice una verificación de actualización después de que transcurran 25 minutos, el servidor pueda responder de manera eficiente con una respuesta HTTP 304 si no hubo actualizaciones en el trabajador de servicio implementado durante ese tiempo.

Si bien algunas interacciones dentro de la app web de la Búsqueda de Google usan navegaciones de estilo de app de una sola página (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 actualización del service worker: clients.claim() y skipWaiting(). En general, hacer clic en la interfaz de la Búsqueda de Google lleva a nuevos documentos HTML. Llamar a skipWaiting garantiza que un service worker actualizado tenga la oportunidad de controlar esas nuevas solicitudes de navegación inmediatamente después de la instalación. Del mismo modo, llamar a clients.claim() significa que el service worker actualizado tiene la oportunidad de 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. Fue el resultado de realizar cuidadosamente pruebas A/B de varias combinaciones de opciones de publicación hasta que encontraron lo que mejor les funcionaba. Los desarrolladores cuya infraestructura de backend les permite implementar actualizaciones más rápido pueden preferir que el navegador verifique si hay una secuencia de comandos de Service Worker actualizada con la mayor frecuencia posible, para lo cual siempre ignoran la caché HTTP. Si compilas una app de una sola página que los usuarios podrían mantener abierta durante un período prolongado, usar skipWaiting() probablemente no sea la opción correcta para ti, ya que corres el riesgo de tener inconsistencias en la caché si permites que se active el nuevo service worker mientras hay clientes de larga duración.

Conclusiones clave

De forma predeterminada, los service workers no son neutrales en cuanto al rendimiento

Agregar un service worker a tu app web significa insertar una pieza adicional de JavaScript que debe cargarse y ejecutarse antes de que tu app web obtenga respuestas a sus solicitudes. Si esas respuestas terminan proviniendo 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 del rendimiento que se obtiene con el enfoque de caché primero. Sin embargo, si sabes que tu service worker siempre debe consultar la red cuando controla solicitudes de navegación, usar la precarga de navegación es una mejora crucial del rendimiento.

Los Service Workers siguen siendo una mejora progresiva

Actualmente, la compatibilidad con Service Workers es mucho mejor que hace un año. Todos los navegadores modernos ahora incluyen al menos cierta compatibilidad con los service workers, pero, lamentablemente, hay algunas funciones avanzadas de service workers, como la sincronización en segundo plano y la precarga de navegación, que no se lanzaron de forma universal. Verificar las funciones para el subconjunto específico de funciones que sabes que necesitas y registrar un trabajador de servicio solo cuando estén presentes sigue siendo un enfoque razonable.

Del mismo modo, si ejecutaste experimentos en condiciones reales y sabes que los dispositivos de gama baja tienen un rendimiento deficiente con la sobrecarga adicional de un trabajador de servicio, también puedes abstenerte de registrar un trabajador de servicio en esos casos.

Debes seguir considerando a los service workers 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 general de la carga.

Medir todo

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

Los detalles específicos de la configuración de mediciones significativas dependen del proveedor de estadísticas que uses y de cómo sueles realizar experimentos en tu configuración de implementación. En este caso de estudio, se detalla un enfoque que utiliza Google Analytics para recopilar métricas y que se basa en la experiencia de usar Service Workers en la app web de Google I/O.

No goles

Si bien muchos en la comunidad de desarrollo web asocian los service workers con las apps web progresivas, crear una "PWA de la Búsqueda de Google" no era un objetivo inicial del equipo. La app web de la Búsqueda de Google no proporciona metadatos en un manifiesto de app web ni alienta a los usuarios a seguir el flujo de Agregar a la pantalla principal. El equipo de Búsqueda está satisfecho con que los usuarios accedan a su app web a través de los puntos de entrada clásicos 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 del lanzamiento inicial fue mejorar progresivamente 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 del service worker y por compartir el material de referencia que se usó para escribir este artículo. Un agradecimiento especial a Philippe Golle, Rajesh Jagannathan y 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, el equipo de la Búsqueda de Google volvió a evaluar los beneficios y las desventajas de su arquitectura actual de Service Worker. El service worker descrito anteriormente se retirará. A medida que evoluciona la infraestructura web de la Búsqueda de Google, es posible que el equipo vuelva a revisar el diseño del service worker.