Un artículo para comprender la optimización del rendimiento del front-end (explicación detallada en 2023)

      A menudo nos encontramos con la palabra optimización del rendimiento en trabajos front-end o entrevistas, parece que esto no es difícil de decir, después de todo, todos pueden hablar de ello. Pero si desea tener una solución de desempeño directa cuando encuentre cuellos de botella en el desempeño en diversos escenarios en el trabajo, o para impresionar al entrevistador durante una entrevista, entonces no puede limitarse a "decir lo que se le ocurra" o "dar una idea aproximada". idea, necesitamos tener un mapa de conocimiento sistemático y profundo desde todos los ángulos. Este artículo también puede considerarse como un resumen de mi conocimiento personal de front-end, porque "optimización del rendimiento" no es solo "optimización", ¿qué significa? Antes de implementar un plan de optimización, primero debes saber por qué necesitas optimizar de esta manera y cuál es el propósito de hacerlo. Esto requiere que tenga una buena comprensión de los principios de framework, js, css, navegador, motor js, red, etc. Por lo tanto, la optimización del rendimiento realmente cubre demasiado conocimiento de front-end, incluso la mayor parte del conocimiento de front-end.

      Primero, hablemos de la esencia del rendimiento del front-end. El front-end es una aplicación de red. El rendimiento de la aplicación está determinado por su eficiencia operativa. Si agregamos la red antes, está relacionado con la eficiencia de la red. Por eso creo que la esencia del rendimiento front-end es el rendimiento de la red y el rendimiento operativo. Por lo tanto, las dos categorías principales en el sistema de optimización del rendimiento del front-end son: red y tiempo de ejecución, y luego subdividimos cada área pequeña de estas dos áreas principales, lo cual es suficiente para tejer un enorme gráfico de conocimiento del front-end.

nivel de red

      Si comparamos la conexión de red con una tubería de agua, si desea abrir una página ahora, se puede ver como si la otra persona tuviera un vaso de agua en la mano y desea conectar el agua a su vaso. Si quieres ir más rápido, hay tres maneras: 1. Hacer que el flujo de la tubería de agua sea más grande y más rápido; 2. Dejar que el otro lado reduzca el agua en la taza; 3. Tengo agua en mi taza y no Necesito el tuyo. El tráfico de la tubería de agua es el ancho de banda de su red, la optimización del protocolo y otros factores que afectan la velocidad de la red; cuando un vaso de agua se reduce, significa compresión, división de código, carga diferida y otros medios para reducir las solicitudes; el último es usar el almacenamiento en caché.

      Hablemos primero de la velocidad de la red. La velocidad de la red no solo la determina el operador del usuario, sino también familiarizándose con los principios de los protocolos de red y ajustando los protocolos de red para optimizar su eficiencia.

      Teóricamente, la red informática es un modelo OSI de siete capas, pero en la práctica puede verse como cinco capas (o modelo de cuatro capas), a saber, la capa física, la capa de enlace de datos, la capa de red, la capa de transporte y la capa de aplicación. Cada capa es responsable de encapsular, desmantelar y analizar sus propios protocolos y realizar sus propias tareas. Por ejemplo, es como la doncella de palacio vistiendo y desvistiendo al emperador capa por capa, tú eres responsable del abrigo y yo de la ropa interior, cada uno cumpliendo sus propias funciones. Como front-end, nos centramos principalmente en la capa de aplicación y la capa de transporte, comenzando con el protocolo HTTP de la capa de aplicación con el que tratamos todos los días.

Optimización del protocolo http.

1. En HTTP/1.1, es necesario evitar alcanzar el límite máximo simultáneo de solicitudes del navegador para el mismo nombre de dominio (normalmente 6 para Chrome).

  • Cuando hay una gran cantidad de solicitudes de recursos de página, puede preparar varios nombres de dominio y utilizar diferentes solicitudes de nombres de dominio para evitar el límite máximo de concurrencia.
  • Se pueden fusionar varios íconos pequeños en una imagen grande, de modo que múltiples recursos de imágenes requieran solo una solicitud. La interfaz muestra los íconos correspondientes (también llamados imágenes de sprites) a través del estilo de posición de fondo de CSS.

2. Reducir el tamaño del encabezado HTTP

  • Por ejemplo, las solicitudes del mismo dominio transportarán automáticamente cookies, lo cual es un desperdicio si no se requiere autenticación. Este tipo de recursos no deberían estar en el mismo dominio que el sitio.

3. Aproveche al máximo la caché HTTP. El almacenamiento en caché puede eliminar directamente las solicitudes y mejorar enormemente el rendimiento de la red.

  • Los navegadores pueden usar valores de encabezado HTTP como no-cache y max-stale de cache-control para controlar si se usa un almacenamiento en caché fuerte, negociar el almacenamiento en caché y si la caducidad del caché aún está disponible y otras funciones.
  • El servidor utiliza valores de encabezado http como max-age, public, stale-when-revalidate de cache-control para controlar el tiempo de caché fuerte, si el servidor proxy puede almacenarlo en caché, cuánto tiempo caduca el caché y cuánto tiempo dura. cuánto tiempo lleva actualizar automáticamente el caché.

4. Actualizar a HTTP/2.0 o superior puede mejorar significativamente el rendimiento de la red. (Debe usar TLS, es decir, https)

5. Optimice HTTPS

       Hay dos aspectos principales que consumen mucho rendimiento de HTTPS:

  • El primer paso es el proceso de protocolo de enlace TLS;
  • El segundo paso es la transmisión de mensajes cifrados simétricamente después del protocolo de enlace.

Para el segundo paso, los algoritmos de cifrado simétrico actuales AES y ChaCha20 tienen un buen rendimiento, y algunos fabricantes de CPU también los han optimizado a nivel de hardware, por lo que se puede decir que el consumo de rendimiento de cifrado en este paso es muy pequeño.

En el primer paso, el proceso de protocolo de enlace TLS no solo aumenta el retraso de la red (puede tardar hasta 2 RTT en el tiempo de ida y vuelta de la red), sino que algunos pasos en el proceso de protocolo de enlace también provocarán pérdidas de rendimiento, como por ejemplo:

      Si se utiliza el algoritmo de acuerdo de clave ECDHE, tanto el cliente como el servidor necesitan generar temporalmente claves públicas y privadas de curva elíptica durante el proceso de protocolo de enlace; cuando el cliente verifica el certificado, accederá al servidor CA para obtener la CRL u OCSP en para verificar si el certificado del servidor ha sido revocado; luego ambas partes calculan el Pre-Master, que es la clave de cifrado simétrica. Para comprender mejor en qué etapa del protocolo de enlace TLS completo se encuentran estos pasos, puede consultar esta imagen:

apretón de manos TLS

HTTPS se puede optimizar utilizando los siguientes medios:

  • Optimización de hardware: el servidor utiliza una CPU que admite el conjunto de instrucciones AES-NI
  • Optimización del software: actualice la versión de Linux y la versión TLS. TLS/1.3 ha optimizado en gran medida la cantidad de apretones de manos, requiriendo solo 1 vez de RTT y admite seguridad directa (lo que significa que si la clave se descifra ahora o en el futuro, no afectará la seguridad de los mensajes interceptados previamente).
  • Optimización de certificados: grapado OCSP. En circunstancias normales, el navegador necesita verificar con la CA si el certificado ha sido revocado, y el servidor puede consultar periódicamente a la CA para conocer el estado del certificado, obtener un resultado de respuesta con una marca de tiempo y firma, y ​​almacenarlo en caché. Cuando un cliente inicia una solicitud de conexión, el servidor enviará directamente el "resultado de la respuesta" al navegador durante el proceso de protocolo de enlace TLS, por lo que el navegador no necesita solicitar la CA.
  • 会话复用 1:Session ID,双方在内存里保留 session,下一次建立连接时 hello 消息里会带上 Session ID,服务器收到后就会从内存中找,如果找到就直接用该会话密钥恢复会话状态,跳过其余的过程。为了安全性,内存中的会话密钥会定期失效。但是它有两个缺点:1. 服务器必须保存每一个客户端的会话密钥,随着客户端的增多,服务器的内存占用也会越大。2. 现在网站服务一般是由多台服务器通过负载均衡提供服务的,客户端再次连接不一定会命中上次访问过的服务器,未能命中那台服务器还是要走完整的 TLS 握手过程。
  • 会话复用 2:Session Ticket,客户端与服务器首次建立连接时,服务器会加密「会话密钥」并作为 Ticket 发给客户端,客户端会保存该 Ticket。这类似 web 开发中验证用户身份的 token 方案。客户端再次连接服务器时,客户端会发送 Ticket,服务器能解密就可以获取上一次的会话密钥,然后验证有效期,如果没问题,就可以恢复会话了,直接开始加密通信。因为只有服务端可以加密解密这个密钥,所以只要能解密说明没有造假。对于集群服务器的话,要确保每台服务器加密 「会话密钥」的密钥是一致的,这样客户端携带 Ticket 访问任意一台服务器时,才都能恢复会话。

Session ID 和 Session Ticket 都不具备前向安全性,因为一旦加密「会话密钥」的密钥被破解或者服务器泄漏了密钥,前面劫持的通信密文都可以被破解。同时面对重放攻击也很困难,所谓的重放攻击就是,假设中间人截获了 post 请求报文,虽然他无法解密其中的信息,但他可以重复使用该非幂等的报文对服务器请求,因为有ticket服务端可以直接复用https。为了减少重放攻击的危害可以将加密的会话密钥设定一个合理的过期时间。


下面是对 http 知识点的详细介绍。

HTTP/0.9

初的版本非常简单,目的是为了快速推广使用,功能也只是简单的 get html,请求报文格式如下:

GET /index.html

 HTTP/1.0

Con el desarrollo de Internet, http necesita cumplir con más funciones, por lo que tiene el familiar encabezado http, código de estado, método de solicitud GET POST HEAD, caché, etc. También puede transmitir archivos binarios como imágenes y videos.

La desventaja de esta versión es que la conexión TCP se desconectará después de cada solicitud y la siguiente solicitud HTTP requiere que TCP restablezca la conexión. Por lo tanto, algunos navegadores han agregado una conexión no estándar: encabezado keep-alive, y el servidor responderá con el mismo encabezado. A través de este acuerdo, TCP puede mantener una conexión larga. Las solicitudes http posteriores pueden reutilizar este TCP hasta que una de las partes cierre activamente él.        

HTTP/1.1

La versión 1.1 se usa ampliamente actualmente. En esta versión, la conexión larga TCP se usa de forma predeterminada. Si desea cerrarla, debe agregar activamente el encabezado Conexión: cerrar.

Además, también tiene un mecanismo de canalización (pipelining): el cliente puede enviar continuamente múltiples solicitudes http en la misma conexión tcp sin esperar el retorno de http. En el pasado, el diseño de las solicitudes HTTP era que solo se podía enviar una solicitud HTTP a la vez en una conexión TCP. Solo después de recibir su valor de retorno se completaba la solicitud HTTP y se podía enviar la siguiente solicitud HTTP. Aunque la versión http/1.1 puede enviar múltiples https continuamente según el mecanismo de canalización, 1.1 aún solo puede devolver respuestas en orden FIFO (primero en entrar, primero en salir) en el servidor, por lo que si el primer http es muy lento durante la respuesta, el los siguientes seguirán bloqueados por el primer http. Al recibir varias respuestas consecutivas, el navegador las dividirá por Longitud del contenido.

Además, se ha agregado codificación de transferencia fragmentada, reemplazando la forma del búfer con una secuencia de flujo. Por ejemplo, para un video, ya no es necesario leerlo completamente en la memoria y luego enviarlo, puede usar la transmisión para enviar una pequeña parte después de leer cada pequeña parte. Utilice el encabezado Transfer-Encoding: fragmentado para activar. Habrá un número hexadecimal delante de cada fragmento para representar la longitud del fragmento. Si el número es 0, significa que el fragmento ha sido enviado. En escenarios como transferencia de archivos grandes o procesamiento de archivos, el uso de esta función puede mejorar la eficiencia y reducir el uso de memoria.

Esta versión tiene las siguientes desventajas:

1. 队头阻塞。必须请求-响应才算一次完整 http 结束,然后才能发下一个 http。如果前一个 http 慢了,会影响下一个的发送时间。同时浏览器对同一域名的 http 请求有最大并发数量的限制,超出就必须等待前面的完成。
2. http 头冗余。可能页面中每个 http 的请求头都基本一样,但每次都要带上这些文本,浪费网络资源。

其实 http1.1 的缺点本质上是因为它一开始的定位就是一个纯文本协议导致的。如果想做乱序发送,要么需要修改协议本身,比如在请求/响应里添加个唯一标识,然后对端做文本的解析,找到对应的顺序。要么需要对 http 协议再做一层封装,将文本转为二进制数据并进行额外的封装处理。根据开闭原则,新增优于修改,所以显然后者方案更合理一点。于是 http/2.0 里会把原数据分割成二进制帧的形式,方便做后续操作,相当于在原来的基础上多了一些步骤,原本的 http 核心没有改变。

HTTP/2.0

新增的改进不仅包括优化了 HTTP/1.1 中积弊已久的多路复用、修复队头阻塞问题,允许设定请求优先级,还包含了一个头部压缩算法(HPACK)。此外, HTTP/2 采用了二进制而非明文来打包、传输客户端和服务器之间的数据。

帧、消息、流和 TCP 连接

Podemos pensar en la versión 2.0 como si agregara una capa de marco binario en http. Un mensaje (una solicitud o respuesta completa se denomina mensaje) se divide en muchas tramas, que contienen: tipo, longitud, banderas, identificador de secuencia, secuencia y carga útil, carga útil de la trama. Al mismo tiempo, también se agrega el concepto abstracto de flujo. El identificador de flujo de cada cuadro representa a qué flujo pertenece. Debido a que http/2.0 se puede enviar fuera de orden sin esperar, el remitente/receptor enviará fuera de orden de acuerdo con al identificador de flujo. Los datos se ensamblan. Para evitar conflictos causados ​​por ID de flujo duplicados en ambos extremos, el flujo iniciado por el cliente tiene un ID impar y el flujo iniciado por el servidor tiene un ID par. El contenido del protocolo original no se ve afectado: el primer encabezado de información en http1.1 se encapsula en el marco de Encabezados y el cuerpo de la solicitud se encapsula en el marco de Datos. Varias solicitudes utilizan solo un canal tcp. Esta iniciativa ha demostrado en la práctica que la carga de nuevas páginas se puede acelerar entre un 11,81% y un 47,7% en comparación con HTTP/1.1. Los métodos de optimización como múltiples nombres de dominio e imágenes de sprites ya no son necesarios en http/2.0.

algoritmo HPACK

El algoritmo HPACK es un algoritmo recientemente introducido en HTTP/2 y se utiliza para comprimir encabezados HTTP. El principio es:

Según el Apéndice A de RFC 7541, el cliente y el servidor mantienen un diccionario estático común (tabla estática), que contiene códigos para nombres de encabezado comunes y combinaciones de nombres y valores de encabezado comunes; el cliente y el servidor siguen la primera entrada El primero en
salir En principio, mantiene un diccionario dinámico común (tabla dinámica) que puede agregar contenido dinámicamente; el
cliente y el servidor admiten la codificación Huffman basada en esta tabla de códigos Huffman estática de acuerdo con el Apéndice B de RFC 7541).

empuje del servidor     

En el pasado, los navegadores necesitaban iniciar solicitudes activamente para obtener datos del servidor. Esto requiere agregar scripts de solicitud js adicionales al sitio web y también debe esperar a que se carguen los recursos js antes de llamar. Esto da como resultado un retraso en el tiempo de solicitud y más solicitudes. HTTP/2 admite la inserción activa del lado del servidor, lo que no requiere que el navegador envíe solicitudes de forma activa, lo que ahorra eficiencia en las solicitudes y optimiza la experiencia de desarrollo. El front-end puede escuchar eventos push desde el servidor a través de EventSource.

 

HTTP/3.0

HTTP/2.0 ha realizado muchas optimizaciones en comparación con su predecesor, como multiplexación, compresión de encabezados, etc., pero debido a que la capa subyacente se basa en TCP, algunos puntos débiles son difíciles de resolver.

bloqueo de cabecera de línea

HTTP se ejecuta sobre TCP. Aunque el encuadre binario ya puede garantizar que no se bloqueen múltiples solicitudes en el nivel HTTP, puede saber por los principios de TCP mencionados anteriormente que TCP también tiene bloqueo y retransmisión de encabezado de línea. El paquete no se devuelve y los siguientes no se enviarán. Por lo tanto, HTTP/2.0 solo resuelve el bloqueo del encabezado de línea en el nivel HTTP y aún está bloqueado en todo el enlace de la red. Sería fantástico si se pudiera utilizar un nuevo protocolo para transmitir más rápido en entornos de red modernos.

Latencia del protocolo de enlace TCP y TLS

TCP tiene 3 apretones de manos, TLS (1.2) tiene 4 apretones de manos y se requieren un total de 3 retrasos RTT para emitir una solicitud http real. Al mismo tiempo, debido a que el mecanismo para evitar la congestión de TCP comienza desde un inicio lento, reducirá aún más la velocidad.

Cambiar de red provoca la reconexión

Sabemos que la unicidad de una conexión TCP se determina en función de la IP y el puerto de ambos extremos. Hoy en día las redes móviles y el transporte están muy desarrollados, cuando entras a la oficina o vas a casa, tu teléfono móvil se conectará automáticamente a WIFI, es muy común que las redes de telefonía móvil cambien de señal en las estaciones base del metro y trenes de alta velocidad en diez segundos. Todos provocarán cambios de IP, invalidando así la conexión TCP anterior. Lo que se manifiesta es que una página web que está abierta a mitad de camino de repente no se puede cargar, y un vídeo que está almacenado en el búfer hasta la mitad no se puede almacenar en el búfer al final.

protocolo QUIC

Los problemas anteriores son inherentes a TCP, para solucionarlos solo podemos cambiar el protocolo, http/3.0 utiliza el protocolo QUIC. Un protocolo completamente nuevo requiere soporte de hardware, lo que inevitablemente llevará mucho tiempo popularizarse, por lo que QUIC se basa en un protocolo UDP existente.

El protocolo QUIC tiene muchas ventajas, tales como:

Sin bloqueo de cabecera


El protocolo QUIC también tiene el concepto de Stream y multiplexación similar a HTTP/2. También puede transmitir múltiples Streams simultáneamente en la misma conexión. Un Stream puede considerarse como una solicitud HTTP.

Dado que el protocolo de transporte utilizado por QUIC es UDP, a UDP no le importa el orden de los paquetes, ni a UDP si los paquetes se pierden.

Sin embargo, el protocolo QUIC aún necesita garantizar la confiabilidad de los paquetes de datos: cada paquete de datos se identifica de forma única mediante un número de secuencia. Cuando se pierde un paquete en un flujo, incluso si llegan otros paquetes en el flujo, HTTP/3 no puede leer los datos. Los datos no se entregarán a HTTP/3 hasta que QUIC retransmita el paquete perdido.

Siempre que el paquete de datos de un determinado flujo se reciba por completo, HTTP/3 puede leer los datos de este flujo. Esto es diferente de HTTP/2, donde si un paquete se pierde en una secuencia, otras secuencias se verán afectadas.

Por lo tanto, no hay dependencia entre múltiples Streams en la conexión QUIC. Todos son independientes. Si un determinado flujo pierde paquetes, solo afectará ese flujo y otros flujos no se verán afectados.

Establecimiento de conexión más rápido

Para los protocolos HTTP/1 y HTTP/2, TCP y TLS están en capas y pertenecen respectivamente a la capa de transporte implementada por el kernel y a la capa de presentación implementada por la biblioteca OpenSSL, por lo que es difícil fusionarlos y es necesario sacudirlos. en lotes Primero el protocolo de enlace TCP y luego el protocolo de enlace TLS.

Aunque HTTP / 3 también requiere un protocolo de enlace QUIC antes de transmitir datos, este proceso de protocolo de enlace solo requiere 1 RTT. El propósito del protocolo de enlace es confirmar el "ID de conexión" de ambas partes, como la migración de la conexión (por ejemplo, la red necesita (se migrará debido al cambio de IP) Implementado en función del ID de conexión.

El protocolo QUIC de HTTP/3 no tiene capas de TLS, pero QUIC contiene TLS internamente. Llevará el "registro" en TLS en su propio marco. Además, QUIC usa TLS 1.3, por lo que solo un RTT puede completar el establecimiento de la conexión. y negociación de claves "simultáneamente" Incluso durante la segunda conexión, el paquete de datos de la aplicación se puede enviar junto con la información del protocolo de enlace QUIC (información de conexión + información TLS) para lograr el efecto de 0-RTT.

Como se muestra en la parte derecha de la figura siguiente, cuando se restaura la sesión HTTP/3, los datos de la carga útil se envían junto con el primer paquete, lo que puede lograr 0-RTT:

 

Migración de conexión

当移动设备的网络从 4G 切换到 WiFi 时,意味着 IP 地址变化了,那么就必须要断开连接,然后重新建立连接,而建立连接的过程包含 TCP 三次握手和 TLS 四次握手的时延,以及 TCP 慢启动的减速过程,给用户的感觉就是网络突然卡顿了一下,因此连接的迁移成本是很高的。如果你在高铁上可能你的 IP hUI连续变化,这会导致你的 TCP 连接不断重新连接。

而 QUIC 协议没有用四元组的方式来“绑定”连接,而是通过连接 ID 来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。

简化帧结构、QPACK 优化头部压缩

HTTP/3 同 HTTP/2 一样采用二进制帧的结构,不同的地方在于 HTTP/2 的二进制帧里需要定义 Stream,而 HTTP/3 自身不需要再定义 Stream,直接使用 QUIC 里的 Stream,于是 HTTP/3 的帧的结构也变简单了。

tramas HTTP/3

  根据帧类型的不同,大体上分为数据帧和控制帧两大类,Headers 帧(HTTP 头部)和 DATA 帧(HTTP 包体)属于数据帧。

HTTP/3 在头部压缩算法这一方面也做了升级,升级成了 QPACK。与 HTTP/2 中的 HPACK 编码方式相似,HTTP/3 中的 QPACK 也采用了静态表、动态表及 Huffman 编码。

对于静态表的变化,HTTP/2 中的 HPACK 的静态表只有 61 项,而 HTTP/3 中的 QPACK 的静态表扩大到 91 项。

HTTP/2 和 HTTP/3 的 Huffman 编码并没有多大不同,但是动态表编解码方式不同。

En la llamada tabla dinámica, después de la primera solicitud-respuesta, ambas partes actualizarán los elementos del encabezado (como algunos encabezados personalizados) que no están incluidos en la tabla estática en sus respectivas tablas dinámicas, y luego solo usarán 1 número para representarlos en transmisiones posteriores, luego la otra parte puede buscar los datos correspondientes de la tabla dinámica de acuerdo con este número, sin tener que transmitir datos largos cada vez, lo que mejora en gran medida la eficiencia de la codificación.

Se puede ver que la tabla dinámica es secuencial. Si el encabezado de la primera solicitud se pierde y las solicitudes posteriores vuelven a encontrar este encabezado, el remitente pensará que la otra parte ya lo ha almacenado en la tabla dinámica, por lo que comprimirá el encabezado. Sin embargo, la otra parte no puede decodificar este encabezado HPACK porque no ha establecido una tabla dinámica. Por lo tanto, la decodificación de solicitudes posteriores debe bloquearse hasta que el paquete de datos perdido en la primera solicitud se retransmita antes de que se pueda lograr la decodificación normal.

QPACK de HTTP/3 resuelve este problema, pero ¿cómo lo resuelve?

QUIC tendrá dos flujos unidireccionales especiales. Solo un extremo del llamado flujo unidireccional puede enviar mensajes. Los flujos bidireccionales se utilizan para transmitir mensajes HTTP. El uso de estos dos flujos unidireccionales:

Uno se llama QPACK Encoder Stream, que se utiliza para pasar un diccionario (valor clave) a la otra parte. Por ejemplo, cuando se enfrenta a un encabezado de solicitud HTTP que no pertenece a una tabla estática, el cliente puede enviar el diccionario a través de este Stream; el otro se llama QPACK Decoder Stream, que se usa para responder a la otra parte y decirle que el diccionario que acaba de enviar se ha actualizado a su tabla dinámica local y que puede usar este diccionario para codificar más adelante. Estos dos flujos unidireccionales especiales se utilizan para sincronizar las tablas dinámicas de ambas partes. La parte codificadora utilizará la tabla dinámica para codificar el encabezado HTTP después de recibir la notificación de confirmación de actualización de la parte decodificadora. Si se pierde el mensaje de actualización de la tabla dinámica, solo hará que algunos encabezados no se compriman y no bloqueará la solicitud HTTP.

Explicación detallada del almacenamiento en caché HTTP

Si no es necesario solicitar un recurso de red y se obtiene directamente del caché local, naturalmente es el más rápido. El mecanismo de caché está definido en el protocolo http, que se divide en caché local (también llamado caché fuerte) y caché que debe verificarse mediante solicitudes (también llamado caché de negociación).

Caché local (caché fuerte)

En http1.0, el encabezado de respuesta de caducidad se utiliza para indicar el tiempo de caducidad del valor de retorno. Dentro de este tiempo, el navegador puede utilizar directamente el caché sin volver a solicitarlo. Después de http1.1, se cambió al encabezado de respuesta Cache-Control, que puede cumplir con más requisitos de almacenamiento en caché. La edad máxima interna indica que el recurso caducará N segundos después de la solicitud. Tenga en cuenta que la edad máxima no es el tiempo que transcurre después de que el navegador recibe la respuesta, sino el tiempo que transcurre después de que se genera la respuesta en el servidor de origen y no tiene nada que ver con el tiempo del navegador. Por lo tanto, si otro servidor de caché en la red almacena la respuesta durante 100 segundos (indicado mediante el campo de encabezado de respuesta Edad), la caché del navegador deducirá 100 segundos de su tiempo de vencimiento. Cuando el caché caduca (ignoramos el impacto de obsoleto mientras se revalida, máximo obsoleto, etc.), el navegador iniciará una solicitud condicional para verificar si el recurso está actualizado (también llamado caché de negociación).

Solicitud condicional (negociar caché)

El encabezado de la solicitud tendrá los campos If-Modified-Since y If-None-Match, que son Last-Modified y etag en el encabezado de respuesta de la última solicitud, respectivamente. Última modificación indica la hora en que se modificó el recurso por última vez, en segundos. Etag es el identificador de una versión específica de un recurso (por ejemplo, se puede generar un etag aplicando hash al contenido). Cuando no hay cambios en If-None-Match o If-Modified-Since, el servidor devolverá una respuesta de código de estado 304. El navegador pensará que el recurso no se ha actualizado y reutilizará el caché local. Dado que el tiempo de modificación del registro de Última modificación está en segundos, si la frecuencia de modificación ocurre dentro de 1 segundo, no se puede juzgar con precisión si se ha actualizado, por lo que la prioridad de juicio de etag es mayor que la de Última modificación.

Si se establece sin caché en Cache-Control, se forzará que no se utilice el almacenamiento en caché fuerte y el almacenamiento en caché negociado se utilizará directamente, es decir, edad máxima = 0. Si se establece no-store, no se utilizará ningún caché.

La estrategia de almacenamiento en caché del navegador para solicitudes es simplemente así. Podemos ver que el almacenamiento en caché está determinado por los encabezados de respuesta y los encabezados de solicitud. Durante el proceso de desarrollo, la puerta de enlace y el navegador generalmente lo han configurado automáticamente para nosotros. Si tiene necesidades específicas, puede personalizarse para utilizar más funciones de control de caché.

Funcionalidad completa de control de caché

Cache-Control también tiene capacidades de control de caché más detalladas. Para conocer el significado completo de los encabezados de respuesta y de solicitud, consulte la siguiente tabla.

encabezado de respuesta

Encabezados de solicitud (solo se enumeran aquellos que no están incluidos en los encabezados de respuesta) |max-stale|El caché todavía está disponible cuando caduca no más de segundos máximos-stale | |min-fresh|Requiere que el servicio de caché devuelva el caché nuevo datos en segundos mínimos, de lo contrario no se utilizará el caché local | |solo si está almacenado en caché| El navegador requiere que el recurso de destino se devuelva solo si el servidor de caché lo ha almacenado en caché |

Optimización del protocolo TCP

Es posible que lo necesite al escribir node. Está bien, no te preocupes, aquellos que solo estén interesados ​​en el front-end puro pueden omitirlo :)

Primero, daremos directamente métodos de optimización para diferentes problemas. Los principios específicos de TCP y por qué ocurren estos fenómenos se presentarán en detalle más adelante.

La siguiente optimización TCP generalmente ocurre en el lado de la solicitud

1. El tamaño de la primera solicitud no debe exceder los 14 kb, lo que puede utilizar de manera efectiva el inicio lento de tcp. Lo mismo se puede hacer con el primer paquete de la página de inicio.

  • Suponiendo que la ventana TCP inicial es 10 y el MSS es 1460, entonces el tamaño del recurso de la primera solicitud no debe exceder los 14600 bytes, que son aproximadamente 14 kb. De esta manera, el tcp del extremo opuesto se puede enviar de una vez, de lo contrario se enviará al menos 2 veces, lo que requiere un RTT (tiempo de ida y vuelta de red) adicional.

2. ¿Qué debo hacer si TCP se bloquea debido al envío frecuente de paquetes de datos pequeños (menos que MSS)?
Esto es muy común en las operaciones del juego (aunque generalmente no se usa el protocolo TCP) y en la línea de comando ssh.

  • Desactivar el algoritmo de Nagel
  • Evite el retraso en la respuesta

Cómo optimizar la retransmisión de pérdida de paquetes TCP

  • Active SACK a través de net.ipv4.tcp_sack (habilitado de forma predeterminada)
  • Active D-SACK a través de net.ipv4.tcp_dsack (habilitado de forma predeterminada)

La siguiente optimización TCP generalmente ocurre en el lado del servidor.

1. La cantidad de solicitudes simultáneas recibidas por el servidor es demasiado alta o se encuentra con un ataque SYN, lo que hace que la cola SYN esté llena y no pueda responder a las solicitudes.

  • Cookies de uso sinóptico
  • Reducir el número de reintentos de sincronización
  • Aumentar el tamaño de la cola de sincronización

2. Demasiados TIME-WAIT hacen que los puertos disponibles estén llenos y no se puedan enviar más solicitudes.

  • Utilice la configuración tcp_max_tw_buckets del sistema operativo para controlar la cantidad de TIME-WAIT concurrentes
  • Si es posible, aumente el rango de puertos y la dirección IP del cliente o servidor.

El método de optimización de TCP anterior se basa en comprender el mecanismo de TCP y ajustar los parámetros del sistema operativo, lo que puede lograr una optimización del rendimiento de la red hasta cierto punto. A continuación, comenzaremos con el mecanismo de implementación de TCP y luego explicaremos qué hacen estos métodos de optimización.

Todos sabemos que se debe establecer una conexión antes de la transmisión TCP, pero de hecho, la transmisión de red no requiere el establecimiento de una conexión. La red fue diseñada originalmente para estar en ráfagas y enviar en cualquier momento, por lo que se abandonó el diseño de la red telefónica. . Por lo general, la llamada conexión TCP es en realidad solo un estado entre dos dispositivos que guarda cierta comunicación entre sí, y no es una conexión real. TCP necesita distinguir si es la misma conexión a través de cinco tuplas, una de las cuales es el protocolo y las cuatro restantes son src_ip, src_port, dst_ip, dst_port (ip de doble extremo y número de puerto). Además, hay cuatro cosas importantes en el encabezado del segmento de mensaje TCP: el número de secuencia es el número de secuencia (seq) del paquete, que indica la posición del primer bit de la parte de datos de este paquete en todo el flujo de datos. , que se utiliza para resolver el caos de paquetes de red. El número de reconocimiento (ack) representa la longitud de los datos recibidos esta vez + la secuencia recibida esta vez, y también es el siguiente número de secuencia de la otra parte (remitente), que se utiliza para confirmar la recepción y resolver el problema de no perder paquetes. . La ventana, también llamada ventana anunciada, es una ventana deslizante que se utiliza para implementar el control de flujo. La bandera TCP es el tipo de paquete, como SYN, FIN, ACK, etc., que se utiliza principalmente para controlar la máquina de estado TCP.

La parte principal se presenta a continuación:

tcp tres veces "apretón de manos"

La esencia del protocolo de enlace de tres vías es conocer el número de secuencia inicial, MSS, ventana y otra información de ambas partes, de modo que los datos se puedan unir de manera ordenada en una situación desordenada, y el máximo Se puede determinar la capacidad de carga de la red y el hardware.

El número de secuencia de secuencia inicial (ISN) es de 32 bits, que es generado por el reloj virtual sumando 1 continuamente con una frecuencia de 4 microsegundos, vuelve a 0 cuando supera 2 ^ 32 y un ciclo dura 4,55 horas. La razón por la que cada establecimiento de conexión no comienza desde 0 es para evitar el problema del conflicto de secuencia entre paquetes nuevos y paquetes antiguos que llegan tarde después de desconectar y restablecer la conexión. 4,55 horas han excedido la vida útil máxima del segmento (MSL) y el paquete anterior ya no existe.

  • El cliente envía un paquete SYN (flags: SYN), suponiendo que la secuencia inicial es x, por lo que seq = x. El cliente tcp ingresa al estado SYN_SEND.
  • El servidor tcp está inicialmente en estado LISTEN. Después de recibirlo, envía un paquete ACK (flags: ACK, SYN). Supongamos que la secuencia inicial es y, seq = y, ack = x + 1. Esto se debe a que flags tiene SYN y ocupa 1 longitud, por lo que a continuación el cliente debe comenzar desde x + 1. El servidor ingresa al estado SYN_RECEIVED.
  • El cliente envía un paquete ACK después de recibirlo, seq = x + 1, ack = y + 1. Luego continúe enviando el contenido real del paquete PSH (suponiendo que la longitud de los datos sea 100), seq = x + 1, ack = y + 1. La razón por la cual el contenido real de seq y ack no cambia con respecto al paquete de confirmación es porque el indicador es ACK, que solo se usa para confirmación y no ocupa la longitud en sí. El cliente entra en el estado ESTABLECIDO.
  • El servidor envía un paquete ACK después de recibirlo, seq = y + 1, ack = x + 101. El servidor entra en el estado ESTABLECIDO.

El cálculo de secuencia y reconocimiento se puede comparar con esta imagen de captura de paquetes (la imagen es de Internet, el número de secuencia que contiene es un número de secuencia relativo)

La transferencia del proceso de envío tcp falló, se recomienda cargar el archivo de imagen directamente.

Tiempos de espera y ataques de SYN

Durante el protocolo de enlace de tres vías, después de que el servidor recibe el paquete SYN y devuelve el SYN-ACK, TCP se encuentra en un estado intermedio de semiconexión. El núcleo del sistema operativo colocará temporalmente la conexión en la cola SYN. ​​Después del protocolo de enlace de tres vías, después de que el servidor recibe el paquete SYN y devuelve el SYN-ACK, TCP se encuentra en un estado intermedio de semiconexión. Si el protocolo de enlace es exitoso, la conexión se colocará en la cola completa. La cola de conexiones. Si el servidor no recibe el ACK del cliente, expirará y volverá a intentarlo. El reintento predeterminado es 5 veces, duplicándose de 1, 1, 2, 4... hasta el quinto tiempo de espera, tomará un total de 63s, momento en el cual tcp se desconectará. Corte esta conexión. Algunos atacantes aprovecharán esta característica para enviar una gran cantidad de paquetes SYN al servidor y luego desconectarse. El servidor tiene que esperar 63 segundos antes de borrar la conexión de la cola SYN, lo que hace que la cola SYN del TCP del servidor esté llena. y no puede continuar brindando servicios. Esta situación también puede ocurrir en condiciones normales de gran concurrencia. En este momento podemos configurar los siguientes parámetros en Linux:

  • tcp_syncookies, puede generar un número de secuencia especial (también llamado cookie) a partir de la información cuádruple, la marca de tiempo incrementada cada 64 segundos y el valor de la opción MSS después de que la cola SYN esté llena. Esta cookie se puede enviar al cliente como una secuencia directamente. Jianlian. De esta forma inteligente, tcp_syncookies guarda parte de la información en SYN sin tener que almacenarla localmente. Los espectadores atentos encontrarán que tcp_syncookies parece requerir sólo dos apretones de manos para establecer una conexión. ¿Por qué no incorporarlo al estándar tcp? Porque también tiene desventajas: 1. La codificación de MSS es de solo 3 bits, por lo que solo se pueden usar 8 valores de MSS como máximo. 2. El servidor debe rechazar otras opciones en el mensaje SYN del cliente que solo se negocian en SYN y SYN+ ACK porque el servidor no tiene lugar para guardar estas opciones, como Wscale y SACK. 3. Operaciones criptográficas agregadas. Por lo tanto, cuando la cola SYN esté llena debido a una gran concurrencia normal, no utilice este método, es solo una versión emasculada de tcp.
  • tcp_synack_retries, úselo para reducir la cantidad de reintentos para el tiempo de espera de SYN-ACK, lo que también reduce el tiempo de limpieza de la cola SYN.
  • tcp_max_syn_backlog, aumenta el número máximo de conexiones SYN, es decir, aumenta la cola SYN.
  • tcp_abort_on_overflow, rechaza la conexión cuando la cola SYN está llena.

tcp "ola" cuatro veces

Suponiendo que el cliente se desconecta primero, la secuencia del ejemplo sigue al último protocolo de enlace.

Antes de cerrar, se ESTABLECE el estado tcp de ambos extremos.

  1. El cliente envía un paquete FIN (banderas: FIN) para indicar que se puede cerrar, seq = x + 101, ack = y + 1. El cliente cambia al estado FIN-WAIT-1.
  2. El servidor recibe este FIN y devuelve un ACK, seq = y + 1, ack = x + 102. El servidor cambia al estado CLOSE-WAIT. Después de recibir este ACK, el cliente cambia al estado FIN-WAIT-2.
  3. Es posible que el servidor tenga algún trabajo sin terminar y, una vez completado, enviará un paquete FIN para decidir cerrar, seq = y + 1, ack = x + 102. El servidor cambia al estado LAST-ACK.
  4. El cliente devuelve un ACK de confirmación después de recibir FIN, seq = x + 102, ack = y + 2. El cliente cambia al estado TIME-WAIT
  5. Después de recibir el ACK del cliente, el servidor cierra directamente la conexión y cambia al estado CERRADO. Si el cliente no vuelve a recibir el FIN del servidor después de esperar 2*MSL, cierra la conexión y cambia al estado CERRADO.

¿Por qué es necesario un TIEMPO DE ESPERA prolongado? 1. Puede evitar que la nueva conexión que reutiliza la tupla de cuatro reciba paquetes antiguos retrasados ​​2. Puede garantizar que el servidor se haya cerrado.

¿Por qué el tiempo TIME-WAIT es 2 * MSL (tiempo máximo de supervivencia del segmento, RFC793 define MSL como 2 minutos y Linux lo establece en 30 segundos)? Porque después de enviar el FIN, el servidor lo reenviará si la espera de ACK se agota. El FIN tiene el tiempo MSL de supervivencia más largo y la retransmisión debe ocurrir antes de esto. El FIN reenviado también tiene el tiempo MSL de supervivencia más largo. Por lo tanto, después de 2 veces el tiempo MSL, el cliente aún no ha recibido el reenvío del servidor, lo que indica que el servidor recibió el ACK y se cerró, por lo que el cliente puede cerrarse.

¿Qué debo hacer si hay demasiados TIEMPOS DE ESPERA generados por la desconexión?

Sabemos que Linux esperará 1 minuto por defecto antes de cerrar la conexión, en este momento el puerto siempre está ocupado. Si hay una gran conexión corta simultánea, demasiados TIME-WAIT pueden hacer que el puerto esté lleno o que la CPU esté demasiado ocupada.

Se recomienda encarecidamente no utilizar las dos últimas configuraciones.

  • tcp_max_tw_buckets, controla el número de TIME-WAIT concurrentes. El valor predeterminado es 180000. Si excede, el sistema destruirá y registrará el registro.
  • ip_local_port_range, aumenta el rango de puertos del cliente
  • Si es posible, aumente el puerto de servicio del servidor (las conexiones tcp se basan en ip y puerto, cuanto más sean, más conexiones estarán disponibles)
  • Si es posible, aumente la IP del cliente o servidor.
  • tcp_tw_reuse, la marca de tiempo debe estar habilitada tanto en el cliente como en el servidor antes de poder usarse. Solo tiene efecto en el cliente. Después de abrir, no es necesario esperar el TIEMPO DE ESPERA, solo toma 1 segundo. Las nuevas conexiones pueden reutilizar directamente este enchufe. ¿Por qué necesito habilitar la marca de tiempo? Debido a que el paquete de la conexión anterior puede circular y finalmente llegar al servidor, y la nueva conexión quíntuple que reutiliza el socket es la misma que el paquete anterior, siempre que la marca de tiempo sea anterior al nuevo paquete, debe ser el paquete. de la antigua conexión, lo cual se puede evitar: se aceptaron por error paquetes antiguos e inútiles.
  • tcp_tw_recycle, el procesamiento de tcp_tw_recycle es más agresivo y reciclará rápidamente el socket en el estado TIME_WAIT. El reciclaje rápido solo se producirá cuando tcp_timestamps y tcp_tw_recycle estén habilitados. Cuando el cliente accede al servidor a través del entorno NAT, el estado TIME_WAIT se generará después de que el servidor se cierre activamente. Si el servidor tiene activadas las opciones tcp_timestamps y tcp_tw_recycle, entonces el tiempo de segmentación TCP desde el mismo host IP de origen dentro de 60 segundos El sello debe incrementarse; de ​​lo contrario, se descartará. Linux ha eliminado la configuración tcp_tw_recycle a partir de la versión del kernel 4.12.

ventana deslizante tcp y control de flujo

El sistema operativo ha abierto un área de caché para tcp, que limita el número máximo de paquetes de datos enviados y recibidos por tcp. Se puede visualizar como una ventana deslizante. La ventana del remitente se llama ventana de envío swnd y la del receptor se llama ventana de recepción rwnd. La longitud de los datos que se enviaron pero no se recibieron acuse de recibo + la longitud de los datos almacenados en el búfer que se enviarán = la longitud total de la ventana de envío. 

Enviar ventana

 

Durante el protocolo de enlace, ambos extremos intercambian valores de ventana y eventualmente se tomará el valor mínimo. Supongamos que el tamaño de la ventana del remitente es 20 y que se envían 10 paquetes al principio, pero aún no se ha recibido ningún acuse de recibo, por lo que solo se pueden colocar 10 paquetes más en el búfer en el futuro. Si el buffer está lleno, no se podrán enviar más datos. Cuando el receptor recibe datos, también los coloca en el buffer. Si la capacidad de procesamiento es menor que la capacidad de envío del par, el buffer se acumulará y la ventana de recepción disponible se hará más pequeña. El valor de la ventana transportado por ack permitirá al remitente para reducir la cantidad de datos enviados. Además, el sistema operativo también ajustará el tamaño del búfer. En este momento, puede ocurrir una situación: la ventana de recepción disponible original es 10, que se ha notificado al par mediante acuse de recibo, pero el sistema operativo reduce repentinamente el búfer. y la ventana se reduce en 15. En cambio, la ventana de recepción disponible se reduce en 15. Debo 5. El remitente recibió previamente que la ventana disponible es 10, por lo que los datos aún se enviarán, pero el receptor no puede procesarlos, por lo que se agota el tiempo de espera. Para evitar esta situación, TCP exige que si el sistema operativo desea modificar el búfer, debe enviar la ventana disponible modificada con anticipación.

Sabemos por el contenido anterior que TCP limita el envío de tráfico a través de las ventanas en ambos extremos. Si la ventana es 0, significa que el envío debe detenerse temporalmente. Cuando el buffer del receptor está lleno y se envía un acuse de recibo con una ventana de 0, y después de un período de tiempo el receptor puede recibir, se enviará un acuse de recibo con una ventana distinta de 0 para notificar al remitente que continúe enviando. Si se pierde este reconocimiento, será muy grave: el remitente nunca sabrá que el receptor puede recibirlo, seguirá esperando y entrará en una situación de punto muerto. Para evitar este problema, el diseño de TCP es que después de que se notifica al remitente que deje de enviar (es decir, después de recibir el acuse de recibo de la ventana 0), iniciará un temporizador y enviará una sonda de ventana cada 30 -60 segundos Después de recibir el mensaje, el receptor debe responder a la ventana actual. Si la detección de ventana es 0 tres veces consecutivas, algunas implementaciones de TCP enviarán paquetes RST para interrumpir la conexión.

如果接收方窗口已经很小了,发送方依然会利用这点窗口发送数据,tcp 头 + ip 头 40 字节,可能数据就几字节,那就非常的不划算。怎么避免这种情况呢?下面看看小数剧包如何优化。        

tcp 小数据包

对于接收方,只要不让它发送小窗口就行,接收方通常才有这种策略:接收窗口如果小于MSS、缓存空间/2的最小值,就告知对端窗口是 0,不要再发数据了,直到窗口大于那个条件。

对于发送方,使用 Nagle 算法,只有满足以下两个条件的其一才会发送:

  • 窗口大小 >= MSS 并且 总数据大小 >= MSS
  • 收到之前发送数据的 ack

如果一条都没满足,它就会一直积攒数据,然后达到某个条件一起发送。

伪代码如下

if there is new data to send then
    if the window size ≥ MSS and available data is ≥ MSS then
        send complete MSS segment now
    else
        if there is unconfirmed data still in the pipe then
            enqueue data in the buffer until an acknowledge is received
        else
            send data immediately
        end if
    end if
end if

Nagle 算法默认是打开的,但是在例如ssh这种数据小、交互多的场景下,Nagle 碰上延迟 ack 会很糟糕,所以需要关闭。(Nagle 算法没有系统全局配置,需要根据各自应用关闭)

说完小数据优化,现在再说回滑动窗口,其实 tcp 最终采用的窗口并不完全由滑动窗口决定,滑动窗口只是防止双端超出收发能力,还要考虑两端之间的网络情况,如果两端收发能力都很强,但此刻网络环境很差,发大量数据只会让网络更拥堵,所以还有一个拥塞窗口,tcp 会取滑动窗口和拥塞窗口的最小值。

tcp 慢启动与拥塞避免

首先要讲一下什么是 MSS,MSS 是一个 tcp segment 最大允许的数据字节长度,是由 MTU(数据链路层最大数据长度,由硬件规定的)减去 ip 头 20 字节 减去 tcp 头 20 字节算出来的,一般是 1460。也就是代表一个 tcp 包最多携带 1460 字节上层的数据。tcp 握手时会在双端协商出最小的 MSS。在实际网络环境中,请求会经过很多中间设备,SYN 里的 MSS 还会被它们修改,最终会是整个路径中的最小值,而不仅仅是两端的最小值。

TCP tiene un cwnd (ventana de congestión) responsable de evitar la congestión de la red, su valor es un múltiplo entero del tamaño del segmento TCP, que representa cuántos paquetes TCP puede enviar a la vez (por conveniencia, comenzamos desde 1 para representarlo). Su valor inicial es muy pequeño, irá aumentando gradualmente hasta que se produzca la pérdida de paquetes y la retransmisión para detectar los recursos de transmisión de red disponibles. En el algoritmo clásico de inicio lento, en el modo de reconocimiento rápido, cada vez que se recibe con éxito un reconocimiento de confirmación, cwnd + 1, por lo que cwnd aumenta exponencialmente, 1, 2, 4, 8, 16... hasta que el umbral de inicio lento ssthresh sea alcanzado (umbral de inicio lento), ssthresh generalmente es igual a max (valor de datos externos/2, 2*SMSS), SMSS es el tamaño máximo de segmento del remitente. Cuando cwnd <ssthresh, se utiliza el algoritmo de inicio lento. Cuando cwnd> = ssthresh, se utiliza el algoritmo para evitar la congestión.

Algoritmo para evitar la congestión, después de recibir cada confirmación, cwnd aumentará en 1/cwnd, es decir, se confirman todos los últimos paquetes enviados, cwnd + 1. A diferencia del algoritmo de inicio lento, el algoritmo para evitar la congestión aumenta linealmente hasta que ocurren dos tipos de retransmisiones y luego disminuye: 1. se produce una retransmisión por tiempo de espera, 2. se produce una retransmisión rápida.

Confirmación rápida/retrasada, retransmisión con tiempo de espera y retransmisión rápida

En el modo de confirmación rápida, el receptor envía una confirmación inmediatamente después de recibir el paquete, pero TCP no devuelve una confirmación cada vez que recibe un paquete, lo que es un desperdicio de ancho de banda de la red. TCP también puede ingresar al modo de confirmación retrasada. El extremo receptor iniciará el temporizador de confirmación retrasada y verificará si la confirmación se debe enviar cada 200 ms. Si hay datos para enviar, también se pueden fusionar con la confirmación. Suponiendo que el remitente envía varios paquetes a la vez, es posible que el par no responda con 10 acuses de recibo, sino que solo responderá con el último acuse de recibo del paquete consecutivo más grande recibido. Por ejemplo, si se transmiten 1, 2, 3,...10, el extremo receptor los recibe todos, por lo que responde con un acuse de recibo de 10, de modo que el extremo emisor sepa que se han recibido los primeros 10 y el siguiente. uno se inicia desde las 11. Si hay una pérdida de paquete en el medio, se devuelve el acuse de recibo antes de la pérdida de paquete.

Retransmisión de tiempo de espera: el remitente iniciará un temporizador después del envío. El tiempo de espera (RTO) es adecuado para configurarlo en un poco mayor que un RTT (tiempo de ida y vuelta del paquete). Si la confirmación de recepción se agota, el paquete de datos se reenviará. Si los datos reenviados se agotan, el tiempo de espera se duplicará. En este momento, ssthresh se convierte en cwnd/2, cwnd se restablece al valor inicial y se utiliza el algoritmo de inicio lento. Se puede ver que cwnd cae por un precipicio, por lo que la ocurrencia de una retransmisión con tiempo de espera tiene un gran impacto en el rendimiento de la red. ¿Tenemos que esperar el RTO antes de retransmitir?

Retransmisión rápida: TCP tiene un diseño de retransmisión rápida. Si el receptor no recibe el paquete en orden, responderá con el mayor acuse de recibo consecutivo. Si el remitente recibe 3 acuses de este tipo seguidos, considerará que el paquete se ha perdido y podrá rápidamente Retransmita ese paquete una vez sin volver al inicio lento. Por ejemplo, el receptor recibió 1, 2 y 4, por lo que respondió con un ACK de 2, y luego recibió 5 y 6. Debido a que 3 fue interrumpido en el medio, aún respondió con un ACK de 2 dos veces. El remitente recibió el mismo acuse de recibo tres veces seguidas, por lo que supo que 3 se había perdido y rápidamente retransmitió 3. El receptor recibe 3 y los datos son continuos, por lo que se devuelve un acuse de recibo de 6 y el remitente puede continuar transmitiendo desde 7. Como en la imagen de abajo:

 

Cuando se produce una retransmisión rápida:

  1. ssthresh = cwnd/2, cwnd = ssthresh + 3, comienza a retransmitir paquetes perdidos e ingresa al algoritmo de recuperación rápida. El motivo de +3 es que se recibieron 3 acuses de recibo duplicados, lo que indica que la red actual puede al menos enviar y recibir normalmente estos 3 paquetes adicionales.
  2. Cuando se recibe un ACK duplicado, la ventana de congestión aumenta en 1
  3. Cuando se recibe el ACK del nuevo paquete de datos, cwnd se establece en el valor de ssthresh en el primer paso.

El algoritmo de retransmisión rápida apareció por primera vez en la versión Tahoe de 4.3BSD y la recuperación rápida apareció por primera vez en la versión Reno de 4.3BSD, también llamada versión Reno del algoritmo de control de congestión TCP. Se puede ver que el algoritmo de retransmisión rápida de Reno tiene como objetivo la retransmisión de un paquete, sin embargo, en la práctica, un tiempo de espera de retransmisión puede causar la retransmisión de muchos paquetes de datos, por lo que cuando se pierden varios paquetes de datos de una ventana de datos, surgen problemas cuando son rápidos. Se activan algoritmos de retransmisión y recuperación rápida. Por lo tanto, aparece NewReno, que está ligeramente modificado en función de la rápida recuperación de Reno y puede recuperar múltiples pérdidas de paquetes dentro de una ventana. Específicamente: Reno sale del estado de recuperación rápida cuando recibe un ACK de datos nuevos, y NewReno necesita recibir confirmación de todos los paquetes de datos en la ventana antes de salir del estado de recuperación rápida, mejorando así aún más el rendimiento.

Cómo retransmitir TCP "con precisión"

Si se produce una pérdida parcial de paquetes, el remitente no sabe qué paquetes se perdieron total o parcialmente. Por ejemplo, si el extremo receptor recibe 1, 2, 4, 5, 6, el extremo emisor puede saber mediante un reconocimiento que los paquetes después de 3 se pierden y desencadenan una retransmisión rápida. Habrá dos decisiones en este momento: 1. Retransmitir únicamente el tercer paquete. 2. No sé si los paquetes 4, 5, 6... también se pierden, así que simplemente retransmito todo después del 3. Ambas opciones no son muy buenas, si solo reenvías 3, si realmente las pierdes después, cada una tendrá que esperar a la retransmisión. Pero si todos se retransmiten directamente, sería un desperdicio perder solo 3. ¿Cómo deberíamos optimizarlo?

La retransmisión rápida solo reduce la posibilidad de activar la retransmisión con tiempo de espera, pero ni la retransmisión rápida ni la retransmisión con tiempo de espera resuelven el problema de saber con precisión si se debe retransmitir una o todas. Existe un método mejor llamado Reconocimiento selectivo (SACK), que debe ser compatible con ambos extremos. Linux lo cambia a través del parámetro net.ipv4.tcp_sack. SACK agregará un dato al encabezado tcp para decirle al remitente qué segmentos de datos se han recibido además del máximo continuo, de modo que el remitente sepa que no es necesario retransmitir los datos. Una imagen vale mas que mil palabras:

También existe Duplicate SACK (D-SACK). Si se pierde el ACK de confirmación del receptor, el remitente pensará erróneamente que el receptor no lo ha recibido, lo que provocará un tiempo de espera y una retransmisión. En este momento, el receptor recibirá datos duplicados. O bien, debido a que el paquete de envío encuentra una congestión en la red, el paquete retransmitido llega antes que el paquete anterior y el receptor también recibirá datos duplicados. En este momento, puede agregar un fragmento de datos SACK al encabezado TCP. El valor es el rango del segmento de datos repetidos. Debido a que el segmento de datos es más pequeño que ACK, el remitente sabe que el receptor ha recibido los datos y no los retransmitirá. .

 

 D-SACK se activa y desactiva en Linux mediante el parámetro net.ipv4.tcp_dsack.

En resumen, la función de SACK y D-SACK es informar al remitente qué paquetes no se han recibido y si los paquetes se han recibido repetidamente. Puede determinar si el paquete de datos se pierde, el acuse de recibo se pierde y los datos El paquete de datos se retrasa por la red o la red se interrumpe. Se copió el paquete de datos.

Un caché más potente: Service Worker

El control de caché HTTP mencionado anteriormente se encuentra principalmente en el backend, y si el caché caduca, aunque hay un caché negociado, todavía habrá más o menos solicitudes, lo que requiere una red y, por lo general, solo puede almacenar en caché las solicitudes de obtención. Estas limitaciones impiden que el front-end pueda ejecutar aplicaciones locales como el cliente. Entonces, ¿hay alguna forma de hacer que el front-end sea completamente caché proxy? Ya sean recursos estáticos o interfaces API, todo lo puede decidir el front-end mismo. Incluso puede convertir la página web en una aplicación local completa como una aplicación. . Este es el Service Worker del que vamos a hablar a continuación, veamos qué características tiene.

Almacenamiento en caché sin conexión

Service Worker puede considerarse como un proxy entre la aplicación y la solicitud de red. Puede interceptar la solicitud y tomar las acciones apropiadas en función de si la red está disponible u otra lógica personalizada. Por ejemplo, puede almacenar en caché HTML, CSS, JS, imágenes y otros recursos después de abrir la aplicación por primera vez. La próxima vez que abra la página web, intercepte la solicitud y devuélvala directamente al caché, para que su aplicación se puede abrir sin conexión. Si el dispositivo se conecta a Internet más tarde, puede solicitar los recursos más recientes en segundo plano y determinar si se ha actualizado. Si se ha actualizado, puede recordarle al usuario que actualice y actualice. En términos de inicio, las aplicaciones front-end que utilizan Service Worker no requieren ninguna red, al igual que las aplicaciones cliente.

notificación de inserción

Service Worker 除了代理请求外,还可以主动让浏览器发出通知,就像 App 的通知一样。你可以使用这个功能做『用户召回』、『热门通知』等。

禁止项

我们的主 js 代码是在渲染线程执行的,而 Service Worker 运行在另一个 worker 线程中,所以它不会阻塞主线程,但也导致有些 api 是不能使用的,比如:操作 dom。同时它被设计为完全异步,所以像XHR和Web Storage这样的同步 api 也是无法使用的,可以用fetch请求。动态import()也是不可以的,只可以静态 import 模块。

Service Worker 出于安全考虑只能运行在 HTTPS 协议上(用 localhost 可以允许 http),毕竟仅它能够接管请求这一功能就已经很强大了,如果被中间人恶意篡改对于普通用户来说可以做到这个网页永远也无法呈现正确的内容。在 FireFox 中,在无痕模式下也无法使用它。

使用方法

Service Worker 的代码应该是一个独立的 js 文件,并且可以通过 https 请求访问,如果你在开发环境,可以允许 http://localhost 这样的地址访问。准备好这些后,首先得在项目代码里注册它:

if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("/js/service-worker.js", {
    scope: "../",
  });
} else {
  console.log("浏览器不支持Service Worker");
}

假设你的网址是https://www.xxx.com,同时在https://www.xxx.com/js/service-worker.js准备了 Service Worker 的 js,/js/service-worker.js实际请求的就是https://www.xxx.com/js/service-worker.js。配置里的scope表示在什么路径下 Service Worker 生效,如果不设置 scope 默认根目录就生效,网页中任何路径都会使用 Service Worker。按例子中的写法,如果设置./则生效路径是/js/*,../则是根目录。

Service Worker 会经过这 3 个生命周期

  1. Download
  2. Install
  3. Activate

La primera es la etapa de descarga. Al ingresar a una página web controlada por un Trabajador de Servicio, esta comenzará a descargarse inmediatamente. Si lo ha descargado antes, la actualización puede determinarse después de esta descarga. La actualización se determinará en las siguientes circunstancias:

  1. Se produjo un salto de página dentro del alcance.
  2. Se activó un evento en Service Worker y no se descargó en 24 horas.

Cuando se descubre que el archivo descargado es nuevo, intentará instalar Install. Los criterios para juzgar si es un archivo nuevo son: primera descarga y comparación byte a byte con el archivo antiguo.

Si es la primera vez que se utiliza Service Worker, se intenta realizar una instalación y, luego de una instalación exitosa, se activa.

Si un Service Worker antiguo ya está en uso, se instalará en segundo plano y no se activará después de la instalación. Esta situación se denomina trabajador en espera. Imagínese que el js antiguo y el nuevo pueden tener conflictos lógicos. El js antiguo se ha estado ejecutando durante un tiempo. Si reemplaza directamente el antiguo por el nuevo y continúa ejecutando la página web, es posible que se bloquee directamente.

¿Cuándo se activará el nuevo Service Worker? Debe esperar hasta que se cierren todas las páginas que utilizan el antiguo Service Worker antes de que el nuevo Service Worker se convierta en un trabajador activo. También puede usar ServiceWorkerGlobalScope.skipWaiting() para omitir la espera directamente. Clients.claim() permite que el nuevo Service Worker controle las páginas existentes actualmente (aquellas que usan el antiguo Service Worker).

Puede saber cuándo se produce la instalación o activación escuchando eventos. El evento más comúnmente utilizado es FetchEvent, que se activa cuando la página inicia una solicitud. También puede usar Cache para almacenar datos en caché y usar FetchEvent.respondWith() para devolver la solicitud. valor de retorno que desee. La siguiente es una forma común de escribir una solicitud de caché:

// 缓存版本,可以升级版本让过去的缓存失效
const VERSION = 1;

const shouldCache = (url: string, method: string) => {
  // 你可以自定义shouldCache去控制哪些请求应该缓存
  return true;
};

// 监听每个请求
self.addEventListener("fetch", async (event) => {
  const { url, method } = event.request;
  event.respondWith(
    shouldCache(url, method)
      ? caches
          // 查找缓存
          .match(event.request)
          .then(async (cacheRes) => {
            if (cacheRes) {
              return cacheRes;
            }
            const awaitFetch = fetch(event.request);
            const awaitCaches = caches.open(VERSION);
            const response = await awaitFetch;
            const cache = await awaitCaches;
            // 放进缓存
            cache.put(event.request, response.clone());
            return response;
          })
          .catch(() => {
            return fetch(event.request);
          })
      : fetch(event.request)
  );
});

上面的代码缓存建立后就不会再更新了,如果你的内容是可能变化的,担心缓存会不新鲜,你可以先返回缓存,以保证用户最快看到内容,然后在 Service Worker 后台请求最新数据,更新到缓存里,最后通知主线程告诉用户有内容更新了,让用户自己决定是否要升级应用。后台请求、判断更新的代码可以自己试着写一下,这里主要讲一讲 Service Worker 如何告诉主线程请求的内容更新了,两个线程之间该如何通信呢?

Service Worker 如何与主线程通信

为什么需要通信呢,首先如果你想 debug,worker 线程里的 console.log 是不会出现在 DevTools 里的。其次,假如你的 Service Worker 资源更新了,是不是通知给主线程,这样你的页面才可以弹出消息提醒询问用户是否要更新。所以通信可能是业务上的刚需。由于 Service Worker 是单独的线程,所以是无法直接和我们的主线程通信的。不过一旦你解决了通信的问题,它就可以有很多妙用,比如多个同站点页面之间可以利用 Service Worker 线程跨页面通信。那么如何解决通信的问题呢,我们可以创建一个消息频道new MessageChannel(),它有两个端口,可以独立收发消息,将其中一个端口port2交给 Service Worker,port1端口留在主线程,那它们就可以通过这个频道通信了。下面的代码将展示如何让两个线程互相通信,从而做到『打印worker线程log』、『通知内容更新』、『升级应用』等功能。 

主线程里的代码 

const messageChannel = new MessageChannel();

// 将port2交给控制当前页面的那个Service Worker
navigator.serviceWorker.controller.postMessage(
  // "messageChannelConnection"是自定义的,用来区分消息类型
  { type: "messageChannelConnection" },
  [messageChannel.port2]
);

messageChannel.port1.onmessage = (message) => {
  // 你可以自定义消息格式来满足不同业务
  if (typeof message.data === "string") {
    // 可以打印来自worker线程的日志
    console.log("from service worker message:", message.data);
  } else if (message.data && typeof message.data === "object") {
    switch (message.data.classification) {
      case "content-update":
        // 你可以自定义不同的消息类型,来做出不同的UI表现,比如『通知用户更新』
        alert("有新内容哦,你可以刷新页面查看");
        break;
      default:
        break;
    }
  }
};

 Service Worker 里的代码

let messageChannelPort: MessagePort;

self.addEventListener("message", onMessage);

// 收到消息
const onMessage = (event: ExtendableMessageEvent) => {
  if (event.data && event.data.type === "messageChannelConnection") {
    // 拿到了port2保存起来
    messageChannelPort = event.ports[0];
  } else if (event.data && event.data.type === "skip-waiting") {
    // 如果主线程发出了"skip-waiting"消息,这里就会直接更新Service Worker,也就让应用升级了。
    self.skipWaiting();
  }
};

// 发送消息
const postMessage = (message: any) => {
  if (messageChannelPort) {
    messageChannelPort.postMessage(message);
  }
};

文件压缩、图片性能、设备像素适配

js、css、图片等资源文件的压缩可以极大的减少大小,对网络性能提升很大。一般后端服务会自动帮我们配置好压缩头,不过我们也可以换成更高效的压缩算法得到更好的压缩比。

content-encoding

Si abre cualquier sitio web y observa su red de recursos, verá que hay un encabezado de codificación de contenido en los encabezados de respuesta, que puede ser gzip, comprimir, deflate, identidad, br y otros valores. Además de que la identidad no representa compresión, puede establecer otros valores para comprimir el archivo y acelerar la transmisión http, el más común de los cuales es gzip. Con soporte de compatibilidad, puede configurar específicamente algunos formatos de compresión más nuevos, como br (Brotli), para lograr una tasa de compresión superior a gzip.

archivo de fuente

Si se requiere una fuente especial en la página y el texto de la página es fijo o pequeño (por ejemplo, solo letras y números), puede recortar manualmente el archivo de fuente para que solo contenga el texto necesario, lo que puede reducir en gran medida el tamaño del archivo. .

Si las palabras de la página son dinámicas, no tienes forma de saber qué palabras serán. En escenarios apropiados, como escenarios donde los usuarios pueden obtener una vista previa de los efectos de fuente al ingresar texto. Los usuarios generalmente solo ingresan unas pocas palabras, por lo que no es necesario introducir todo el paquete de fuentes, pero no sabes qué ingresará el usuario. Por lo tanto, puede dejar que el backend (o crear una capa de bff basada en nodejs) genere dinámicamente un archivo de fuente que contenga solo unas pocas palabras y se lo devuelva según las palabras que desee. Aunque hay una petición de consulta más, los archivos de fuentes de varios Mb o incluso de más de diez Mb se pueden reducir a unos pocos kb de tamaño.

Formato de imagen

Las imágenes generalmente no se comprimen mediante los métodos anteriores, porque esos formatos de imagen ya se han comprimido para usted y la compresión nuevamente no tendrá mucho efecto. Por lo tanto, la elección del formato de imagen es la clave para afectar el tamaño y la calidad de la imagen. En términos generales, cuanto menor sea la compresión, más tardará y peor será la calidad de la imagen. Pero no es absoluto: el nuevo formato puede hacer todo mejor que el antiguo, pero tiene poca compatibilidad. Entonces necesitas encontrar un equilibrio.

En términos de formatos de imagen, además de los comunes PNG-8/PNG-24, JPEG y GIF, prestamos más atención a otros formatos de imagen más nuevos:

  • WebP
  • JPEG-XL
  • AVIF

Utilice una tabla para compararlos en términos de tipo de imagen, canal de transparencia, animación, rendimiento de codificación y decodificación, algoritmo de compresión, compatibilidad de color, uso de memoria y compatibilidad:

 

 Desde una perspectiva de desarrollo técnico, se da prioridad al uso de formatos de imagen relativamente nuevos: WebP, JPEG XL y AVIF. JPEG XL es muy prometedor para reemplazar el formato de imagen tradicional, pero la compatibilidad sigue siendo muy pobre. La compatibilidad AVIF es mejor que JPEG XL, conservando una alta calidad de imagen después de la compresión y evitando molestos artefactos de compresión y otros problemas. Sin embargo, las velocidades de decodificación y codificación no son tan rápidas como las de JPEG XL y no se admite la renderización progresiva. WebP es compatible con básicamente todos los navegadores excepto IE. Para imágenes complejas (como fotografías), el rendimiento de la codificación sin pérdidas de WebP no es bueno, pero el rendimiento de la codificación con pérdidas es muy bueno. La velocidad de decodificación de imágenes de WebP con calidad similar no es muy diferente de la de JPEG XL, pero la relación de compresión de archivos se puede mejorar mucho. Por ahora, parece que si desea mejorar el rendimiento de la imagen de su sitio web, sería mejor utilizar WebP en lugar del formato tradicional.        

Uso del elemento Imagen

Entonces, ¿hay algo que pueda ayudarnos automáticamente a utilizar formatos de imagen similares a WebP, AVIF y JPEG XL que mencionamos anteriormente en navegadores que admiten algunos formatos de imagen modernos, mientras que los navegadores que no admiten recurren al método JPEG y PNG normal? La especificación HTML5 agrega un nuevo elemento de imagen. El elemento <picture> proporciona versiones de una imagen para diferentes escenarios de visualización/dispositivo al contener cero o más elementos <source> y un elemento <img>. El navegador seleccionará el elemento <source> secundario que mejor coincida o, si no hay coincidencia, seleccionará la URL en el atributo src del elemento <img>. Luego, la imagen seleccionada se representa en el espacio ocupado por el elemento <img>. 

<picture>
  <!-- 可能是一些对兼容性有要求的,但是性能表现更好的现代图片格式-->
  <source src="image.avif" type="image/avif" />
  <source src="image.jxl" type="image/jxl" />
  <source src="image.webp" type="image/webp" />

  <!-- 最终的兜底方案-->
  <img src="image.jpg" type="image/jpeg" />
</picture>

Adaptación del tamaño de la imagen: píxeles físicos, píxeles independientes del dispositivo

Si desea un excelente rendimiento de imagen, debe utilizar tamaños de imagen adecuados para elementos de diferentes tamaños. Si se muestra una imagen de 500*500 en un área de 100*100 píxeles, esto es obviamente un desperdicio; por el contrario, una imagen de 100*100 en 500*500 píxeles es muy borrosa, lo que reduce la experiencia del usuario. Antes de hablar sobre la adaptación de tamaño, primero debemos hablar sobre qué son los píxeles independientes del dispositivo y los píxeles físicos, y qué es DPR.

Cuando escribimos ancho: 100 px en CSS, lo que se muestra en la pantalla es en realidad un píxel independiente del dispositivo (también llamado píxel lógico) de 100 px de largo, que no son necesariamente los 100 píxeles (píxeles físicos) de la pantalla. En la pantalla original, los píxeles independientes del dispositivo y los píxeles físicos eran 1:1, es decir, ancho: 1 px corresponde a 1 píxel de punto emisor de luz en la pantalla. Con el posterior desarrollo de la tecnología de visualización, los píxeles de las pantallas del mismo tamaño se han vuelto cada vez más refinados, tal vez la posición de un píxel ahora esté compuesta por 4 píxeles. Esto aporta una mayor densidad de píxeles y una mejor experiencia visual, pero también crea un problema. Si ancho: 1px representa un punto de luz de píxel como antes, la misma página se reducirá en este dispositivo porque los píxeles ahora son más pequeños. Para solucionar este problema, los fabricantes crearon el concepto de píxeles independientes del dispositivo, que no son píxeles reales, sino lógicos. Si 1 píxel en el dispositivo ahora se reemplaza por 2 píxeles más pequeños, entonces la proporción de píxeles del dispositivo (DPR) del dispositivo es 2, y una imagen dibujada con un ancho: 1 px se dibujará con 2 píxeles, por lo que el tamaño y solía ser se consistente. De manera similar, en un dispositivo con una pantalla más fina, suponiendo que se compone de 3 píxeles más pequeños en lugar del tamaño tradicional de 1 píxel, entonces su DPR es 3 y el ancho: 1 px en realidad está dibujado por 3 píxeles. Ahora puede comprender por qué el entrevistador hizo preguntas como "Cómo dibujar un borde de 1 px", porque con un DPR alto su 1 px en realidad no es 1 px.

Entonces podemos obtener esta ecuación de píxeles: 1 píxel CSS = 1 píxel independiente del dispositivo = píxel físico * DPR.

Proporcionar imágenes apropiadas para diferentes pantallas del DPR.

Por lo tanto, aunque nuestros elementos img son todos de 100 píxeles, el tamaño de imagen óptimo que necesitamos mostrar es en realidad diferente en diferentes dispositivos DPR. Cuando DPR = 2, se debe mostrar una imagen de 200 px, y cuando DPR = 3, se debe mostrar una imagen de 300 px; de lo contrario, se producirán condiciones borrosas.

Entonces, ¿cuáles son algunas posibles soluciones?

Opción 1: gráficos múltiples simples y crudos

El DPR más alto en dispositivos comunes ahora es 3, por lo que la forma más sencilla es utilizar la visualización de imagen 3x más alta de forma predeterminada. Pero esto provocará una gran pérdida de ancho de banda, ralentizará el rendimiento de la red y reducirá la experiencia del usuario, lo que definitivamente no coincide con el "estilo" de nuestro artículo.

Opción 2: Consulta de los medios

Podemos usar consultas de medios @media para aplicar diferentes CSS según el DPR del dispositivo actual.

#img {
  background: url([email protected]);
}
@media (device-pixel-ratio: 2) {
  #img {
    background: url([email protected]);
  }
}
@media (device-pixel-ratio: 3) {
  #img {
    background: url([email protected]);
  }
}

 La ventaja de esta solución es que puede mostrar imágenes con diferentes aumentos bajo diferentes DPR.

Las desventajas de esta solución son:

  • Hay muchas ramas lógicas, y no solo hay dispositivos en el mercado con DPR = 2 o 3, sino también algunos dispositivos con un DPR decimal, es necesario escribir mucho código para cubrirlo todo.
  • Problemas de compatibilidad de sintaxis, por ejemplo en algunos navegadores es -webkit-min-device-pixel-ratio. Puedes solucionarlo con autoprefixer, pero eso también introduce un coste adicional.

 Opción 3: sintaxis de conjunto de imágenes CSS

#img {
  /* 不支持 image-set 的浏览器*/
  background-image: url("../[email protected]");

  /* 支持 image-set 的浏览器*/
  background-image: image-set(
    url("./[email protected]") 2x,
    url("./[email protected]") 3x
  );
}

 Entre ellos, 2x y 3x coinciden con diferentes DPR. Las desventajas de la solución de conjunto de imágenes son las mismas que las de las consultas de medios, por lo que no entraré en detalles. La ventaja es que es más específico que las consultas de medios y te permite fingir ser una ola.

Opción 4: atributo del elemento srcset

<img src="[email protected]" srcset="[email protected] 2x, [email protected] 3x" />

Los 2x y 3x en el interior indican que coinciden diferentes DPR, y [email protected] es el resultado final. Las ventajas y desventajas son las mismas que las del conjunto de imágenes, la ventaja puede ser que no requiere escribir CSS y es más conciso.

Opción 5: atributo srcset combinado con atributo de tamaños 

<img
  sizes="(min-width: 600px) 600px, 300px"
  src="[email protected]"
  srcset="[email protected] 300w, [email protected] 600w, [email protected] 900w"
/>

tamaños="(min-width: 600px) 600px, 300px" significa: si el ancho de píxeles CSS actual de la pantalla es mayor o igual a 600px, el ancho CSS de la imagen es 600px. De lo contrario, el ancho CSS de la imagen es 300 px. Debido a que su diseño puede ser flexible, el tamaño del elemento img puede ser diferente en diferentes tamaños de pantalla. Las otras soluciones anteriores solo se pueden juzgar en función de DPR, lo que no se puede lograr. tamaños también requiere que @media cambie el ancho de la imagen según el umbral de ancho.

srcset="[email protected] 300w, [email protected] 600w, [email protected] 900w" Los 300w, 600w y 900w internos se denominan descriptores de ancho. Si está en un dispositivo con un DPR de 2 y los píxeles CSS del elemento img son 300 según los tamaños, entonces los píxeles físicos reales son 600, por lo que se utilizará una imagen de 600w.

La desventaja de esta solución sigue siendo la misma que antes: es necesario escribir imágenes diferentes para diferentes DPR. Pero tiene la ventaja única de que puede cambiar de manera flexible la resolución de la imagen real de acuerdo con el tamaño del elemento img en un diseño responsivo. Por eso recomiendo la opción cinco.

Carga diferida y decodificación asincrónica de imágenes.

La carga diferida de imágenes significa que cuando la página no se ha desplazado al área de destino, las imágenes allí no se solicitan ni se muestran, para acelerar la visualización del contenido en el área visible. Las especificaciones de front-end actuales son muy ricas: tenemos js, html y otros métodos para implementar la carga diferida de imágenes. 

Opción 1: usar onscroll en js

Esta es una solución simple y tosca: obtenga la distancia de todas las imágenes en la página desde la parte superior de la ventana gráfica a través de getBoundingClientRectAPI, monitoree el desplazamiento de la página a través del evento de desplazamiento y luego calcule qué imágenes aparecen en el área visible según la altura de la ventana gráfica. y establezca el atributo src del elemento img.Valor para controlar la carga de la imagen.

La ventaja de esta solución es que la lógica es simple y fácil de entender, no se utilizan nuevas API y la compatibilidad es buena.

Las desventajas de esta solución son:

  1. Es necesario introducir js, lo que genera cierta cantidad de código y costo de cálculo.
  2. Necesidad de obtener la información de posición de todos los elementos de la imagen, lo que puede provocar un reflujo adicional
  3. Necesidad de monitorear el desplazamiento en todo momento y activar devoluciones de llamada con frecuencia
  4. Si una lista de desplazamiento está anidada en la página, esta solución no puede conocer la visibilidad de los elementos en la lista de desplazamiento anidada y requiere una escritura más compleja.

Opción 2: usar IntersectionObserver en js

A través de la API IntersectionObserver de HTML5, Intersection Observer coopera con el atributo isIntersecting del elemento de monitoreo para determinar si el elemento está dentro del área visible y puede implementar una solución de carga diferida para imágenes con mejor rendimiento que el monitoreo en desplazamiento. El elemento observado activará una devolución de llamada cuando aparezca o desaparezca en el área visible, y también se puede controlar el umbral de la relación de apariencia. Consulte la documentación de mdn para obtener más detalles.

Las ventajas de esta solución son:

  1. El rendimiento es mucho mejor que onscroll. No necesita monitorear el desplazamiento en todo momento, ni necesita obtener la posición del elemento. La visibilidad la conoce el hilo de renderizado al dibujar y no es necesario juzgarla a través de js. .Esta forma de escribir es más natural.
  2. Realmente puede conocer la visibilidad de los elementos, por ejemplo, si un elemento es bloqueado por un elemento de nivel superior, es invisible, incluso si ya aparece en el área visible. Esto es algo que la solución onscroll no puede hacer.

Las desventajas de esta solución son:

  1. Es necesario introducir js, lo que genera cierta cantidad de código y costo de cálculo.
  2. Los dispositivos más antiguos no son compatibles y necesitan usar polyfill

Opción 3: visibilidad de contenido estilo CSS

Si un elemento con el estilo content-visibility: auto no está actualmente en la pantalla, el elemento no se representará. Este método puede reducir el trabajo de dibujo y representación de elementos en áreas no visibles, pero se solicitan recursos de imagen cuando se analiza HTML, por lo que esta solución CSS no puede implementar realmente la carga diferida de imágenes.

Solución 4: carga de atributos HTML = diferido

<img src="xxx.png" loading="lazy" />

Solución de decodificación asincrónica de imágenes

Como todos sabemos, las imágenes como jpeg, png, etc. están codificadas, si desea que la GPU las reconozca y las procese, es necesario decodificarlas. Si ciertos formatos de imagen se decodifican muy lentamente, afectará la representación de otros contenidos. Por lo tanto, HTML5 agregó un nuevo atributo de decodificación para indicarle al navegador cómo analizar los datos de la imagen.

Sus valores opcionales son los siguientes:

  • sincronización: decodifica la imagen de forma sincrónica para garantizar que se muestre junto con otro contenido.
  • async: decodifica imágenes de forma asincrónica para acelerar la visualización de otro contenido.
  • auto: Modo predeterminado, lo que indica que no se prefiere el modo de decodificación. Depende del navegador decidir qué método es más apropiado para el usuario.
<img src="xxx.jpeg" decoding="async" />

Esto permite al navegador decodificar la imagen de forma asincrónica, acelerando la visualización de otro contenido. Esta es una parte opcional de su plan de optimización de imágenes.

Resumen de optimización del rendimiento de la imagen

En general, para optimizar el rendimiento de la imagen, es necesario:

  1. Elija un formato de imagen con alta tasa de compresión, velocidad de decodificación rápida y buena calidad de imagen.
  2. Adapte la resolución de imagen adecuada según el DPR real y el tamaño del elemento
  3. Utilice una solución de mejor rendimiento para la carga diferida de imágenes y utilice decodificación asincrónica según la situación.

Optimización de herramientas de construcción

Actualmente existen muchas herramientas populares de construcción/empaquetado de front-end, como las antiguas webpack, rollupque se han vuelto populares en los últimos años vite, las snowpacknuevas fuerzas esbuild, swcetc. turbopackAlgunos de ellos están implementados en js, otros están escritos en lenguajes de alto rendimiento como go y rust, y algunas herramientas de construcción utilizan funciones de esm para empaquetado bajo demanda. Pero estas son optimizaciones de velocidad durante el desarrollo o la construcción y tienen poco que ver con el rendimiento del cliente, por lo que no entraremos en ellas aquí. Hablaremos principalmente de la optimización del rendimiento de la red a través del empaquetado del entorno de producción. Aunque estas herramientas tienen varias configuraciones, los puntos de optimización más utilizados son: compresión de código, división de código, extracción de código público, extracción de CSS, uso de recursos CDN, etc., pero los métodos de configuración son diferentes, solo consulte la documentación para esto. muchos Funciona desde el primer momento. Es posible que algunas personas no entiendan muy bien estas palabras, aquí tienes una explicación.

No hay nada que explicar sobre la compresión de código: significa reemplazar nombres de variables, eliminar nuevas líneas, eliminar espacios, etc., para hacer el código más pequeño.

El propósito de la división del código es que, por ejemplo, en SPA, la página A se redirige desde la página de inicio a través del enrutamiento local, por lo que no es necesario empaquetar el componente de la página A con la aplicación principal en la página de inicio, porque los usuarios no necesariamente pueden saltar a él y empaquetarlo. Al mismo tiempo, aumenta el tamaño del paquete de la página de inicio y afecta la velocidad de la primera pantalla. Entonces, en algunas herramientas de compilación, puede usar la importación dinámica ( import('PageA.js')), y la herramienta de compilación empaquetará el código de la página A al que se hace referencia en la página de inicio en un nuevo paquete, por ejemplo a.js. Cuando el usuario hace clic en la página de inicio para saltar a la página A, a.jsel código del componente interno se solicitará automáticamente y luego la ruta se cambiará y representará. Algunos marcos funcionarán de inmediato y no requerirán que escriba importaciones dinámicas. Simplemente defina la ruta y automáticamente separará el código por usted, como el marco nextjs de React. Este es solo un escenario de uso de la separación de códigos. En resumen, siempre que no desee que un determinado código de módulo se empaquete con la aplicación principal, puede dividirlos para obtener un mejor rendimiento del primer lote de paquetes js.

El propósito de la extracción de código común es que, suponiendo que está escribiendo un SPA, utiliza la biblioteca ramda en las páginas A, B y C, y el código se divide en estas tres páginas, y ahora son tres paquetes independientes: a.js, b.js, c.js. Por lo tanto, según la lógica normal, la biblioteca ramda, como su dependencia, también se incluirá en estos tres paquetes, lo que significa que estas tres páginas tienen códigos ramda duplicados sin ningún motivo. Entonces esto no es bueno, la mejor manera es colocar la biblioteca ramda como un paquete separado en la aplicación principal, de modo que solo sea necesario solicitarla una vez y ABC pueda usar esta biblioteca. Esto es lo que hace la extracción de código común. Por ejemplo, en webpack puedes definir cuántas veces se depende repetidamente de un módulo antes de que se extraiga en un paquete separado como un fragmento común.

optimization: {
  // split-chunk-plugin 是webpack内置的插件 作用是自动将多个入口用到的公共文件抽离出来单独打包
  splitChunks: {
    chunks: 'all',
    // 最小30kb
    minSize: 30000,
    // 被引用至少6次
    minChunks: 6,
  },
}

 Sin embargo, a partir de webpack4, puede ayudarlo automáticamente a optimizar a través del modo. En realidad, no necesita preocuparse por esto. Puede leer más sobre la documentación de la herramienta de compilación que utiliza para evitar optimizaciones innecesarias.

El propósito de la extracción de CSS es que, por ejemplo, si solo usa css-loader + style-loader en el paquete web, su CSS se compilará en js y js lo ayudará a insertar el estilo al representar el estilo. Luego, su js se hará más grande de manera invisible y la representación de los estilos css se retrasará hasta que se ejecute js, y js generalmente se empaqueta al final de la página, es decir, hasta que se completen la solicitud y ejecución final de js, su página permanecer No hay estilo. La situación ideal debería ser que css y dom se analicen y representen en paralelo, por lo que se debe extraer css. Empaquetará css en un archivo css por separado y lo colocará en la etiqueta de enlace al comienzo de html en lugar de ponerlo en js.

Optimización de la sacudida de árboles

Sabemos que la herramienta de empaquetado nos ayudará a eliminar el código muerto basado en Tree Shaking de esm al empaquetar.

Por ejemplo, aquí está bar.js

// bar.js
export const fn1 = () => {};

export const fn2 = () => {};

 Luego use su función fn1 en index.js

// index.js
import { fn1 } from "./bar.js";

fn1();

 Si usamos index.js como punto de entrada para el empaquetado, eventualmente se eliminará fn2.

Pero la sacudida del árbol fallará en algunos escenarios. Debe requerir que su código no tenga "efectos secundarios", es decir, que no pueda afectar el mundo exterior durante la inicialización, similar a los efectos secundarios en la programación funcional.

Mira el siguiente ejemplo:

// bar.js
export const fn3 = () => {};
console.log(fn3);

export const fn4 = () => {};
window.fn4 = fn4;

export const fn5 = () => {};
// index.js
import { fn5 } from "./bar.js";

fn5();

Aunque no se utilizan fn3 y fn4, se incluirán en el paquete final. Porque hay efectos secundarios al declararlos: imprimir, modificar variables externas. Si no los conserva, pueden ocurrir errores que no sean consistentes con las expectativas. Por ejemplo, cree que la ventana ha sido cambiada, pero en realidad no es así. Las propiedades de los objetos pueden tener definidores, e incluso puede haber más errores inesperados.

Además, tampoco están permitidos estos métodos de escritura:

// bar.js
const a = () => {};
const b = () => {};
export default { a, b };

// import o from './bar.js'
// o.a()
// bar.js
module.exports = {
  a: () => {},
  b: () => {},
};

// import o from './bar.js'
// o.a()

No se pueden poner cosas exportadas en un objeto. Tree Shaking de esm es un análisis estático y no puede saber qué se hace en tiempo de ejecución. También se utiliza la sintaxis modular Commonjs. Aunque la herramienta de empaquetado puede ser compatible con su uso mixto, puede hacer que Tree Shaking falle fácilmente.

Por lo tanto, para utilizar completamente la función Tree Shaking, debes prestar atención al método de escritura. Antes de conectarse, puede utilizar la herramienta de análisis de paquetes para ver qué paquetes tienen un tamaño anormal.

Optimización de la pila de tecnología front-end

Además de afectar la velocidad del tiempo de ejecución, la selección de la pila de tecnología también puede tener un impacto en la velocidad de la red.

Reemplazado por una biblioteca más pequeña. Por ejemplo, si usa lodash, incluso si solo usa una función, empaquetará todo el contenido, porque está basado en commonjs y no realiza Tree Shaking en absoluto. Si es sensible a la velocidad de la página web , puedes considerar usar algo más: Reemplazo de biblioteca.

Redundancia de código causada por métodos de desarrollo. Por ejemplo, si está utilizando soluciones de estilo como sass, less, CSS nativo, componente con estilo, emoción, etc. Es fácil para usted escribir código de estilo repetido. Por ejemplo, el componente A y el componente B tienen un ancho: 120 px;. Lo más probable es que lo escriba dos veces. Es difícil para usted lograr una reutilización detallada (casi nadie lo hará). repetir una línea de estilo) (tal vez 7 u 8 líneas sean iguales antes de pensar en reutilizarlas). Cuanto más grande sea el proyecto, más antiguo será, más códigos de estilo se repetirán, y entonces sus archivos de recursos se harán más y más grandes. . Puedes cambiar a tailwindcss, que es una biblioteca CSS atómica. Si necesitas ancho: 120px; estilo, en reaccionar puedes escribir <div className="w-[120px]"></div>, todas iguales Las fórmulas son todos escritos así y todos reutilizan la misma clase. El uso de tailwind puede mantener sus recursos CSS lo suficientemente pequeños sin ninguna sobrecarga de tiempo de ejecución. Al mismo tiempo, debido a que sigue los componentes, puede aprovechar la agitación del árbol de esm. Algunos componentes que ya no se utilizan se eliminarán automáticamente del paquete junto con sus estilos. En el caso de sass, css y otras soluciones, es difícil eliminar automáticamente los estilos que ya no se utilizan en un archivo css. Además, las soluciones CSS-in-JS, como el componente con estilo y la emoción, también pueden lograr la sacudida del árbol, pero tienen problemas con la duplicación de código y la sobrecarga del tiempo de ejecución. También hay algunas deficiencias del viento de cola, como no admitir versiones inferiores de nodejs y la gramática tiene un costo de aprendizaje.

nivel de tiempo de ejecución

El tiempo de ejecución se refiere principalmente al proceso de ejecución de JavaScript y representación de páginas, que implica optimización de la pila de tecnología, optimización de subprocesos múltiples, optimización de nivel V8, optimización de representación del navegador, etc. 

Cómo optimizar el tiempo de renderizado

El tiempo de renderizado no sólo se ve afectado por la complejidad de su DOM y estilo, sino que también se ve afectado por muchos aspectos.

Hay muchos tipos de tareas en el hilo de renderizado.

Antes de hablar de esta sección, primero debemos hablar del concepto de tareas. Es posible que algunas personas ya comprendan algo sobre las tareas macro, como el código en el script y algunas devoluciones de llamada (eventos, setTimeout, ajax, etc.) Estas son tareas macro. Pero es posible que solo comprenda los detalles de la tarea macro y le falte una comprensión más amplia de la tarea. Solo entendiéndola desde un nivel superior podrá comprender realmente por qué se deben bloquear js y el renderizado, y por qué no hay brecha entre dos macro. tareas una al lado de la otra y es posible que no se implementen rápidamente.

Cuando abre una página, el navegador iniciará un proceso de renderizado con un hilo de renderizado. La mayoría de las cosas del front-end se ejecutan en este hilo de renderizado, como dom, renderizado css y ejecución js. Dado que solo hay un hilo, para procesar tareas que consumen mucho tiempo sin bloquearse, se diseña una cola de tareas. Cuando se encuentran operaciones como solicitudes e IO, se entregarán a otros hilos. Una vez completadas, las devoluciones de llamada La tarea principal en esta cola siempre se sondea y ejecuta en la cola y en el hilo de renderizado. La mayoría de las tareas de js pueden entenderse como tareas macro. Pero no es solo js. La representación de la página también es una tarea para el hilo de representación. Puede ver la tarea responsable de la representación en rendimiento en DevTools (se compone de una serie de tareas como analizar HTML, diseño, pintura, etc.), js La ejecución de la llamada tarea macro es en realidad la tarea Evaluar script (que incluye compilar código, almacenar en caché el código de script y otras subtareas, responsables de la compilación en tiempo de ejecución, el código de almacenamiento en caché, etc.), que inicialmente ser una subtarea en la tarea Analizar HTML. También hay muchas tareas integradas, como la recolección de basura de GC. También hay un tipo especial de tarea llamada microtareas, que en rendimiento son Ejecutar microtareas, se generan en macrotareas y se colocan en la cola de microtareas dentro de las macrotareas. Cuando se ejecuta la macrotarea y todas las pilas de ejecución salen, habrá un punto de control. Si hay microtareas en la cola de microtareas, se ejecutarán todas. Se pueden crear microtareas como Promise.then, queueMicrotask, eventos MutationObserver, nextTick en el nodo, etc.

Por lo tanto, ahora que entendemos las tareas en el hilo de renderizado, no es difícil descubrir que, dado que el renderizado en sí también es una tarea, debe estar secuencial en la cola con tareas js y otras tareas, y debe ejecutarse una por una. Así es como se produce el bloqueo. Echemos un vistazo a la relación de bloqueo entre varios recursos.

Para dar un ejemplo típico de representación de bloqueo de js, puede crear un archivo html usted mismo y probarlo:

<html>
  <head>
    <title>Test</title>
  </head>
  <body>
    <script>
      const endTime = Date.now() + 3000;
      while (Date.now() <= endTime) {}
    </script>
    <div>This is page</div>
  </body>
</html>

 El hilo de renderizado primero ejecuta la tarea Parse HTML y encuentra un script durante el análisis del dom, por lo que se ejecuta Evaluate Script. El código se ejecutará durante 3 segundos antes de finalizar y luego continuará analizando y representando el siguiente <div> Esta es la página</div>, por lo que la página tardará 3 segundos en aparecer. Si el script es un recurso remoto, la solicitud también bloqueará el análisis y la representación del DOM subyacente.

Podemos optimizarlo a través del atributo defer del script.Defer retrasará el tiempo de ejecución del script hasta después de que se analice el DOM y antes del evento DOMContentLoaded.

<html>
  <head>
    <title>Test</title>
  </head>
  <body>
    <script defer src="xxx.very_slow.js"></script>
    <div>This is page</div>
  </body>
</html>

 De esta manera, no hay necesidad de perder el tiempo esperando solicitudes y, al mismo tiempo, también puede garantizar que js deba analizarse en el dom para obtener elementos de manera más segura, y múltiples scripts diferidos garantizarán el orden de ejecución original. O puede lograr un efecto similar escribiendo el guión directamente al final de la página. No se preocupe si la solicitud de secuencia de comandos escrita en la parte inferior se retrasa. Los navegadores generalmente tienen un mecanismo de optimización que escaneará las solicitudes de todos los recursos en HTML con anticipación y las solicitará previamente cuando comience el análisis del documento.

El script también tiene otro atributo async. Si el recurso js aún se está solicitando, la solicitud y ejecución de js también se omitirán. El siguiente contenido se analizará primero y se ejecutará inmediatamente después de que se complete la solicitud de js. Por lo tanto, su tiempo de ejecución no es fijo y depende de cuándo finaliza la solicitud, y no se garantiza el orden de ejecución de múltiples scripts asíncronos.

¿CSS bloqueará el renderizado y js?

Solo recuerde una conclusión aquí: la solicitud y el análisis de css no bloquearán el análisis del dom a continuación, pero bloquearán la representación del árbol de representación y la ejecución de js.

En cuanto a por qué está diseñado así:

El árbol de renderizado está bloqueado porque originalmente es el producto de la hoja de estilo en cascada aplicada al árbol dom, por lo que debe esperar a CSS. Aunque está diseñado para no esperar a CSS, no hay problema. Puedes renderizar el árbol de dom. primero y luego renderizar el árbol de renderizado completo, pero renderizarlo dos veces es un desperdicio y la experiencia del usuario de tener un árbol DOM desnudo aparece directamente no es buena.

La razón por la cual js será bloqueado por css puede ser porque el estilo se puede modificar en js. Si se ejecuta el js posterior y se modifica el estilo primero, y luego se aplica el css anterior, el resultado del estilo será inconsistente. Con el orden de escritura del código, solo puede continuar. Es un desperdicio representar los estilos en js dos veces para lograr el efecto real esperado. Y el estilo del elemento se puede obtener en js. Si se ejecuta el siguiente js antes de analizar la solicitud css, el estilo obtenido no coincidirá con la situación real.

Entonces, en resumen, aunque css no bloqueará directamente el análisis de dom, bloqueará la representación del árbol de renderizado e indirectamente bloqueará el análisis de dom al bloquear la ejecución de js.

Si está interesado, puede crear usted mismo un servicio de nodo para experimentar. Al controlar el tiempo de respuesta de los recursos, puede probar la influencia mutua de varios recursos.

¿Por qué el procesamiento del navegador tarda tanto? ¿Qué es el proceso de renderizado?

Convertir un HTML en una página generalmente requiere los siguientes pasos:

  • Generar árbol dom: al obtener el html, ¿qué hizo exactamente el navegador para que apareciera la página? Primero, analice previamente todas las solicitudes de recursos internas y emita solicitudes previas. Luego, el html se analiza léxicamente y gramaticalmente. Cuando encuentra etiquetas de elementos como <body> y <div> y atributos como class e id, analiza y genera un árbol dom. Durante este período, puede encontrar etiquetas css y js, como <style>, <link> y <script>. La solicitud de recursos css no bloqueará el análisis del árbol dom. Si el árbol dom ha terminado de analizar el css, no se analizará hasta entonces Bloquear la creación del árbol de renderizado y los árboles de diseño posteriores, etc. Si encuentra js, ya sea que se trate de ejecución de código o solicitud de recursos, esperará hasta que se completen todas las ejecuciones antes de continuar con el análisis dom, a menos que la etiqueta del script tenga atributos asíncronos o diferidos. Si hay un recurso css delante de js, js no se ejecutará hasta que se solicite/analice css, lo que hará que css bloquee indirectamente el análisis de dom. Si se encuentra código relacionado con CSS, el siguiente paso se realizará para analizar el CSS en una hoja de estilo.
  • Generación de hoja de estilo: css también se somete a análisis léxico y gramatical, y algunos de sus valores serán estandarizados. ¿Qué es la estandarización? Por ejemplo, la columna font-weight: negrita y flex-flow: nowrap que escribiste no son en realidad estilos CSS estándar, sino una abreviatura. Debe convertirse en un valor que el motor pueda entender: font-weight : 500, dirección flexible: columna, envoltura flexible: nowrap. Finalmente, el texto serializado se convierte en una hoja de estilo estructurada.
  • Generar árbol de renderizado: con el árbol DOM y la hoja de estilo, puede agregar estilos al DOM correspondiente mediante herencia, prioridad del selector CSS y otras reglas de estilo. El selector CSS coincidirá con las condiciones de derecha a izquierda, de modo que el número de coincidencias sea relativamente mínimo y, finalmente, formar un árbol de renderizado con estilos.
  • Diseño: algunos DOM no se muestran, como display: none, por lo que se formará un árbol de diseño basado en el árbol de renderizado, que solo contiene nodos que aparecerán en el futuro para evitar cálculos no válidos. Al mismo tiempo, la etapa de diseño calculará la información de posición de diseño de cada elemento, lo que lleva mucho tiempo y las posiciones de los elementos se afectarán entre sí.
  • Capa: Luego se formarán diferentes capas de acuerdo con algunos estilos especiales, como posición: absoluta, transformación, opacidad, etc. El nodo raíz y el desplazamiento también se contarán como una sola capa. Debido a que los diferentes diseños de capas generalmente no se afectan entre sí, la creación de capas puede reducir los costos de diseño en actualizaciones posteriores y también facilitar que las capas compuestas posteriores realicen transformaciones especiales en capas individuales.
  • Pintura (dibujo): en realidad no se trata de dibujar en la pantalla, sino de generar sus propios comandos de dibujo para cada capa. Estos comandos son comandos básicos para el dibujo de GPU, como dibujar una línea recta, etc.
  • Compuesto (compuesto): en este paso, la CPU ya no ejecutará la tarea y la tarea se entregará a la GPU para su procesamiento, por lo que si js está bloqueado, no afectará este hilo. La aceleración de hardware CSS también ocurre en este hilo. La lista de comandos de dibujo en la etapa de pintura se entregará a la capa de composición. La capa de composición dividirá el área cerca de la ventana gráfica actual en mosaicos, en unidades de 512 px, y renderizará primero el área del mosaico. Otras páginas que no están cerca de la La ventana gráfica puede esperar hasta que estén libres. Renderizar nuevamente. La capa de composición pasa los comandos de dibujo a la GPU a través del grupo de subprocesos de rasterización para dibujar y generar mapas de bits. Dado que estos mapas de bits pertenecen a cada capa, la capa de composición debe sintetizar estas capas en un mapa de bits. Una vez que se ha rasterizado una capa, una capa de composición puede componer varias capas, apilándolas en el orden correcto para formar el renderizado final. Este proceso generalmente se realiza en la GPU para reducir la carga de trabajo de la CPU y mejorar el rendimiento de renderizado.

Explique la rasterización: la rasterización es un concepto de gráficos por computadora. En la capa de composición, convierta gráficos vectoriales, texto, imágenes y otros elementos de la capa en imágenes de mapa de bits o rasterizadas. Esto permite que estos elementos se representen y muestren más rápido porque los mapas de bits se procesan de manera más eficiente en el hardware de gráficos. La capa de composición puede dibujar el contenido que se debe representar en un área de memoria fuera de la pantalla en lugar de representarlo directamente en la pantalla. Esto evita problemas de rendimiento causados ​​por dibujar directamente en la pantalla y permite que el navegador optimice el contenido fuera de la pantalla en segundo plano. Al rasterizar el contenido de la capa, el navegador puede aprovechar mejor la aceleración del hardware de gráficos para la renderización. Las unidades de procesamiento de gráficos (GPU) en computadoras y dispositivos móviles modernos pueden procesar de manera eficiente imágenes de mapas de bits, proporcionando animaciones más fluidas y velocidades de renderizado más rápidas.

  • Pantalla: Espere a que el monitor envíe una señal de sincronización, lo que significa que el siguiente fotograma está a punto de mostrarse. El mapa de bits de la capa compuesta se entregará al componente biz en el proceso del navegador y el mapa de bits se colocará en el búfer posterior. Cuando el monitor muestre el siguiente cuadro, los búferes frontal y posterior se intercambiarán para mostrar la última imagen de la página. La devolución de llamada de requestAnimationFrame en js también se activa porque la señal de sincronización sabe que el siguiente fotograma está a punto de representarse. También hay sincronización vertical en el juego.

Durante el renderizado, estos pasos se ejecutan en secuencia como una tubería. Si la tubería comienza a ejecutarse desde un determinado paso, inevitablemente ejecutará todos los pasos siguientes hasta el final.

Por lo tanto, si la página se actualiza porque se modifican los estilos relacionados con la posición/diseño, el diseño de 2 etapas se volverá a activar para recalcular el diseño, lo que se denomina reflujo (reflujo o reflujo). Dado que es necesario calcular las posiciones de una gran cantidad de elementos y las posiciones se afectarán entre sí, se puede ver que este paso lleva mucho tiempo. Al mismo tiempo también se ejecutarán todos los pasos posteriores, como pintura y composite, por lo que el reflujo debe ir acompañado de repintado.

Si la página se actualiza debido a una modificación de estilo independiente de la posición (como color de fondo, color), solo se reactivará desde la pintura de la cuarta etapa, porque los datos de los que depende el proceso anterior no han cambiado. Esto regenerará el comando de dibujo y luego lo rasterizará y lo compondrá en la capa de composición. Todo el proceso sigue siendo muy rápido, por lo que volver a dibujar es mucho más rápido que reordenar.

Cómo utilizar los principios de renderizado para mejorar el rendimiento

El navegador en sí tiene algunos métodos de optimización. Por ejemplo, no tiene que preocuparse por el color: rojo; ancho: 120 px que causa pintura repetida debido a problemas de orden, y no tiene que preocuparse por el deterioro del rendimiento causado por múltiples imágenes consecutivas. modificaciones a estilos y múltiples elementos adjuntos consecutivos. El navegador no comienza a procesarse inmediatamente después de que lo modificas, simplemente coloca las actualizaciones en la cola de espera y luego las actualiza en lotes después de una cierta cantidad de modificaciones o un cierto período de tiempo.

Cuando escribimos código para actualizar la página, el principio es activar la menor cantidad posible de canalizaciones de renderizado. Comenzar desde la etapa de pintura será mucho más rápido que la etapa de diseño. Aquí hay algunas consideraciones comunes:

  1. Evite el reflujo indirecto. Además de modificar directamente los estilos relacionados con la posición, algunas situaciones pueden modificar indirectamente el diseño. Por ejemplo, si el tamaño de su cuadro no es el de borde y el ancho no es fijo, entonces si agrega o modifica el ancho de borde, afectará el ancho del modelo de cuadro y la posición del diseño. Por ejemplo, <img /> no especifica una altura, lo que hace que la altura de la imagen aumente después de la carga, lo que provoca que la página se redistribuya.
  2. 读写分离原则。js 获取元素位置信息可能会触发强制 reflow,比如getBoundingClientRect、offsetTop等。前面已经讲了浏览器是批量更新,会有等待队列,所以你在获取位置信息时,可能等待队列还有更新没有清空,页面不是最新的。浏览器为了保证你拿到的数据是准确的,会去强制清空队列,强制 reflow 页面。而当你第二次获取位置信息,并且期间没有发生更新的话,等待队列是空的,那么就不会再触发 reflow 了。所以如果你要批量修改一批元素尺寸,并且获取它们的尺寸信息,就千万不能这样写:
const elements = document.querySelectorAll(".target");
const count = 1000;
for (let i = 0; i < count; i++) {
  // 将元素width增加20px
  elements[i].style.width = parseInt(elements[i].style.width) + 20 + "px";
  // 获取该元素最新宽度
  console.log(elements[i].getBoundingClientRect().width);
}

 经过上面对浏览器批量更新和强制 reflow 的解释,可以看出这样写是很有问题的,页面会 reflow 1000 次!因为每次你修改style.width后,浏览器会把这次更新放进等待队列里。这一步没什么问题。可是紧接着你就开始获取这个元素的宽度,于是浏览器为了知道最新宽度,就会清空等待队列,跳过批量更新,强制去 reflow 页面。然后一直这样循环 1000 次。

而如果你这样写,1000 次尺寸修改就只会 reflow 一次:

const elements = document.querySelectorAll(".target");
const count = 1000;
for (let i = 0; i < count; i++) {
  // 将元素width增加20px
  elements[i].style.width = parseInt(elements[i].style.width) + 20 + "px";
}
for (let i = 0; i < count; i++) {
  // 获取该元素最新宽度
  console.log(elements[i].getBoundingClientRect().width);
}

 css 硬件加速

在合成层之前的那些阶段的计算基本都是 CPU 执行的,CPU 计算单元远没有 GPU 多,虽然对复杂任务能力强大,但面对简单重复的任务速度比 GPU 慢很多。如果页面的渲染直接从合成层开始,只由 GPU 计算,速度必然会非常快,这就是硬件加速。哪些方式可以开启呢。

像transform 3D、opacity这些 css 样式,由于不涉及回流和重绘,仅仅是图层的变换,所以会跳过前面的 layout、paint 阶段,直接交给合成层,由 GPU 对图层做一些简单变换即可,GPU 处理这些事情非常简单。另外还有一个 css 属性叫will-change,它是专门用来做硬件加速的,它会提前告诉 GPU 哪些属性将来会变化,提前做好准备。

Una cosa a tener en cuenta es que cuando usa js para modificar estilos, incluso si modifica los estilos anteriores que pueden acelerarse por hardware, seguirán pasando por la CPU. ¿Todavía recuerdas el canal de renderizado? JS solo puede modificar el contenido del árbol DOM, lo que inevitablemente desencadenará cambios en el DOM, por lo que comenzará desde el primer paso del canal de renderizado hasta el final, y no comenzará directamente desde la composición. capa.

Dado que el estilo modificado por js no se puede acelerar por hardware, ¿cómo se puede modificar? Puede utilizar métodos que no sean js, como animación o transición. Puede experimentar y ver si la animación sigue funcionando cuando js bloquea completamente la página.

        Cómo registrar el rendimiento y solucionar problemas de congelaciones de renderizado

Para juzgar si una página está atascada o no, no basta con probarla usted mismo y sentir que no está atascada. No puede dar datos cuantitativos basados ​​en consideraciones subjetivas. Debe partir de los datos de su trabajo para convencer a los demás. . 

1. Métricas del desarrollador

Si desea comprobar el rendimiento de abrir una determinada página localmente, puede ir a Lighthouse en DevTools. 

 

 Haga clic en el botón Analizar carga de página para generar un informe de rendimiento.

 

 Incluye indicadores de rendimiento como tiempo del primer sorteo y tiempo de interacción, indicadores de accesibilidad, indicadores de experiencia de usuario, indicadores SEO, PWA (aplicaciones web progresivas), etc.

Si desea solucionar la causa del retraso en el procesamiento nativo. Puede ir a rendimiento en DevTools, que muestra claramente las tareas de menor a mayor. Podemos ver la palabra Long Task, que se refiere a una tarea larga. La definición de una tarea larga es bloquear el hilo principal durante 50 milisegundos. o tareas anteriores. Puede hacer clic en una tarea larga para ver en detalle lo que se hace en esta tarea (en el ejemplo, querySelectorAll tarda demasiado, por lo que es necesario optimizarlo).

2. Monitoreo de usuarios reales

Lo anterior solo es adecuado para la resolución de problemas temporales durante el proceso de desarrollo, y el equipo objetivo es solo su computadora y las condiciones de su red. Es imposible saber cómo los usuarios en diferentes entornos de red, dispositivos y ubicaciones geográficas realizarán el desempeño real del proyecto una vez que esté en línea. Entonces, si desea conocer los indicadores de desempeño reales, necesita otros métodos.

Para juzgar si el desempeño es bueno o malo, primero se debe definir un conjunto claro de nombres de indicadores. Entonces, ¿qué indicadores se necesitan para juzgar el desempeño?

 

 ¿Cómo obtener estos indicadores? Los navegadores modernos generalmente tienen API de rendimiento, en las que puede ver muchos datos de rendimiento detallados. Aunque algunos de los datos anteriores no están disponibles directamente, puede calcularlos a través de algunas API básicas.

 

Puede ver que hay, por ejemplo: eventCounts: número de eventos, memoria: uso de memoria, navegación: método de apertura de página, número de redireccionamientos, tiempo: tiempo de consulta DNS, tiempo de conexión TCP, tiempo de respuesta, análisis de dom y tiempo de renderizado. tiempo interactivo, etc.

Además, Performance también tiene algunas API muy útiles, como performance.getEntries().

Devolverá una matriz que enumera todos los recursos y el tiempo invertido en momentos clave. Entre ellos se encuentran los indicadores de primera pintura FP y primera pintura de contenido FCP. Si solo desea encontrar ciertos informes de rendimiento específicos, puede usar performance.getEntriesByName() y performance.getEntriesByType() para filtrar.

Hablemos de cómo se debe calcular el indicador TTI (Time to Interactive time to interact):

  1. Primero obtenga el tiempo del primer dibujo de contenido (FCP) de First Contentful Paint, que se puede obtener a través de performance.getEntries() arriba.
  2. Busque una ventana silenciosa con una duración de al menos 5 segundos en la dirección de avance de la línea de tiempo, donde la ventana silenciosa se define como: sin tareas largas (tareas largas, js bloquea tareas durante más de 50 ms) y no más de dos redes Solicitudes GET en proceso.
  3. Busque la última tarea larga antes de la ventana de silencio en dirección inversa a lo largo de la línea de tiempo. Si no se encuentra ninguna tarea larga, la ejecución se detiene en el paso FCP.
  4. TTI es la hora de finalización de la última tarea larga antes de la ventana de silencio (igual que el valor FCP si no se encuentra ninguna tarea larga).

Quizás la dificultad sea que la gente no sabe cómo realizar tareas largas. Hay una clase llamada PerformanceObserver que se puede utilizar para monitorear los datos de rendimiento. Agregue longtask a EntryTypes para obtener información de tareas largas. También puede agregar más tipos para obtener otros indicadores de rendimiento. Para obtener más detalles, puede consultar la documentación de esta clase. El siguiente es un ejemplo de monitoreo de tareas largas:

const observer = new PerformanceObserver(function (list) {
  const perfEntries = list.getEntries();
  for (let i = 0; i < perfEntries.length; i++) {
    // 这里可以处理长任务通知:
    // 比如报告分析和监控
    // ...
  }
});
// register observer for long task notifications
observer.observe({ entryTypes: ["longtask"] });
// 之后如果有长任务执行的话,会把执行数据放入性能检测队列
// 于是就会在observer中得到"longtask" entries.

Después de escribir el código para contar varios indicadores de rendimiento (o usar una biblioteca ya preparada directamente), puede ocultarlo en la página del usuario e informarlo al backend de estadísticas de rendimiento cuando el usuario abre la página.

Cómo optimizar js

Hay muchos ángulos de optimización para js, que deben dividirse en diferentes escenarios, por lo que tenemos que hablar de ello desde la perspectiva de la selección de pila de tecnología, subprocesos múltiples y v8. 

Selección de pila de tecnología

1. Selección de solución de renderizado de páginas

1. Representación CSR del lado del navegador: los marcos front-end de Spa como reaccionar y vue son bastante populares ahora, y las aplicaciones de spa controladas por estado pueden lograr un cambio de página rápido. Pero la desventaja que esto conlleva es que toda la lógica está en el js del lado del navegador, lo que hace que el proceso de inicio de la primera pantalla sea demasiado largo.

2. Representación del lado del servidor SSR: debido a que el proceso de representación innato de spa (renderización del lado del navegador CSR) es más largo que el de la representación del servidor (SSR), pasará por la solicitud html -> solicitud js -> ejecución js -> js renderizando contenido -> Después de montar el dom, solicite la interfaz -> actualice el contenido. La representación del lado del servidor solo requiere los siguientes pasos: solicitud html -> representación del contenido de la página -> solicitud js -> ejecución js de agregar eventos. Cuando se trata de renderizar el contenido de la página, el renderizado del lado del servidor es mucho más rápido que SPA, lo cual es muy adecuado para escenarios donde se espera que los usuarios vean el contenido lo antes posible.

你可以使用 react 的 nextjs 框架或者 vue 的 nuxtjs 框架,它们可以在前后端同一套代码的情况下做到服务器端同构渲染。本质原理是 nodejs 带来的服务端运行环境、虚拟 dom 这个抽象层带来的多平台渲染能力。在同一套代码的情况下,浏览器和服务器都可以渲染(服务器渲染出来的是 html 文本),并且在页面二次跳转时依然可以采用 spa 的方式,在保证首屏速度的前提下不丢失 spa 的页面切换速度。同时两大框架的 SSR 性能也在不断优化,比如在 React18 的 SSR 中,新的renderToPipeableStream api 可以流式渲染 html 和具有Suspense特性,可以跳过耗时的任务,让用户更快看见主要页面。还可以选择性注水(Selective Hydration),将不需要同步加载的组件选择性地用 lazy 和 Suspense 包起来(和客户端渲染时一样),优化主要页面的可交互时间,间接做到了 ssr 中的代码分割。

3. SSG 静态页面生成:比如 react 的 nextjs 框架还支持生成静态站点,直接在打包时运行你的组件生成最终的 html,你的静态页面可以没有运行时,达到极致的打开速度。

4. App 客户端渲染:如果你的前端页面是放在 App 里的,可以让客户端实现和服务端渲染一样的机制,这时 App 中打开页面就类似服务端渲染。或者更简单的做法是将你的前端 spa 包放在客户端包里,也可以做到秒开。它们的最大提速点其实是用户在安装 App 时也下载了前端资源。

二、前端框架的取舍

En el proceso moderno de desarrollo front-end, el desarrollo del marco generalmente se elige sin dudarlo. Pero si su proyecto no es complicado ahora ni en el futuro, y está buscando mucho rendimiento, entonces en realidad no necesita usar marcos controlados por estado como reaccionar y vue. Aunque puede usarlos para disfrutar de la conveniencia de desarrollo de actualizar solo la página de estado modificándola, también existe un costo de rendimiento al mejorar DX (experiencia del desarrollador). En primer lugar, debido a la introducción de tiempo de ejecución adicional, la cantidad de js ha aumentado. En segundo lugar, debido a que son al menos renderizados a nivel de componente, es decir, después de que cambia el estado, los componentes correspondientes se volverán a ejecutar por completo, por lo que el DOM virtual obtenido debe pasar por diff para lograr un mejor rendimiento de renderizado del navegador. Estos enlaces adicionales significan que definitivamente no es tan rápido como usar js o jquery directamente para modificar con precisión el dom. Entonces, si su proyecto no es complicado ahora ni en el futuro y desea que sea lo suficientemente rápido y liviano, puede implementarlo directamente con js o jquery.

3. Optimización del marco.

Si elige el marco React, generalmente necesitará realizar alguna optimización adicional durante el proceso de desarrollo. Por ejemplo, use useMemo para almacenar en caché los datos cuando las dependencias permanecen sin cambios, useCallback para almacenar en caché las funciones cuando las dependencias permanecen sin cambios, debe ComponentUpdate del componente de clase para determinar si el componente necesita actualizarse, etc. Debido a que React determina internamente si actualizar en función de si la dirección de referencia de la variable cambia, incluso si dos objetos o literales de matriz son exactamente iguales, son dos valores diferentes.

Además, si es posible, intenta seguir usando la última versión. Generalmente, las nuevas versiones optimizarán el rendimiento.

Por ejemplo, React18 agrega un mecanismo de prioridad de tareas para evitar que tareas largas bloqueen la interacción de la página. Las actualizaciones de baja prioridad serán interrumpidas por actualizaciones de alta prioridad (como los clics y las entradas del usuario), y las actualizaciones de baja prioridad continuarán hasta que se completen las actualizaciones de alta prioridad. De esta manera, los usuarios sentirán que la respuesta es oportuna al momento de interactuar. Puede utilizar useTransition y useDeferredValue para generar actualizaciones de baja prioridad.

Además, React18 también optimiza las actualizaciones por lotes. En el pasado, las actualizaciones por lotes se implementaban mediante un mecanismo de bloqueo, similar a:

lock();
// 锁住了,更新只是放进队列并不会真的更新

update();
update();

unlock();
// 解锁,批量更新

 这就会限制你只能在固定的一些地方才能使用到批量更新,比如生命周期、hooks、react 事件里,因为 react 以外的地方是不会有锁的。并且,如果你用了 setTimeout、ajax 等脱离当前宏任务的 api,里面的更新也会不能批量更新。

lock();

fetch("xxx").then(() => {
  update();
  update();
});

unlock();
// updates已经脱离了当前宏任务,一定在unlock之后才执行,这时已经没有锁了,两次update就会让react渲染两次。

 React18 的批量更新是基于优先级设计的,所以不需要一定在 react 规定的地方才能批量更新。

四、框架生态的选型

除了框架本身以外,它的生态选型也会对性能有影响。vue 的生态一般比较固定,但 react 的生态非常丰富,为了追求性能,就需要对不同库的特性和原理有所了解。我们这里主要谈谈全局状态管理和样式方案的选型。

在状态管理库的选型中,react-redux在极端条件下就可能有性能问题。注意这里说的是react-redux,而不是redux,redux只是一个通用库,本身很简单,可以用于各种地方,无法直接谈论性能好坏。react-redux是一个用来让 react 能够使用 redux 的库,因为redux状态每次都是一个新的引用,所以react-redux无法知道哪些依赖状态的组件需要更新,就需要用selector比较一下前后的值是否变化。每个依赖了全局状态的组件的selector都需要执行一遍,如果selector里面逻辑比较重或者组件数量比较多,就会产生性能问题。可以试试mbox,它的基本原理和 vue 一样,是基于拦截对象的getter和setter来触发更新的,所以天然知道哪个组件需要更新。另外zustand使用体验不错也比较推荐,虽然它也是基于redux那一套,但用起来非常方便,还可以在组件外使用,不需要大量模板代码。

Entre las soluciones de estilo, solo la solución css-in-js es posible con tiempo de ejecución, como styled-component yemotion. Pero no del todo, algunas bibliotecas css-in-js eliminarán el tiempo de ejecución cuando no calcules dinámicamente estilos basados ​​en accesorios. Si su estilo se calcula en función de los accesorios del componente, entonces el tiempo de ejecución es esencial. Calculará el CSS al ejecutar el componente js y luego agregará la etiqueta de estilo por usted. Esto traerá dos problemas, uno es el costo de rendimiento y el otro es que el tiempo de representación del estilo se retrasa hasta la etapa de ejecución de js. Puede optimizar esto utilizando soluciones distintas a css-in-js, como css, sass, less, stylus y tailwind. El más recomendado aquí es Tailwind, que se mencionó cuando se habló de optimización a nivel de red. No solo tiene tiempo de ejecución cero, sino que también le permite reutilizar estilos completamente debido a la atomización, y sus recursos CSS serán muy pequeños.

js multiproceso

Anteriormente sabíamos que las tareas js bloquearán la representación de la página, pero ¿qué pasa si se necesita una tarea larga para el negocio? Como hash de archivos grandes. En este momento, podemos iniciar otro hilo, dejar que ejecute esta larga tarea y decirle al hilo principal el resultado final. 

trabajador web 

const myWorker = new Worker("worker.js");

myWorker.postMessage(value);

myWorker.onmessage = (e) => {
  const computeResult = e.data;
};
// worker.js
onmessage = (e) => {
  const receivedData = e.data;
  const result = compute(receivedData);
  postMessage(result);
};

Solo se puede acceder a un Web Worker mediante el hilo que lo creó, que es la ventana de la página que lo creó.

Trabajador compartido

Se puede acceder a Shared Worker mediante varias ventanas, iframes y trabajadores diferentes.

const myWorker = new SharedWorker("worker.js");

myWorker.port.postMessage(value);

myWorker.port.onmessage = (e) => {
  const computeValue = e.data;
};
// worker.js
onconnect = (e) => {
  const port = e.ports[0];

  port.onmessage = (e) => {
    const receivedData = e.data;
    const result = compute(receivedData);
    port.postMessage(result);
  };
};

Acerca de la seguridad del hilo

Debido a que Web Worker ha controlado cuidadosamente los puntos de comunicación con otros subprocesos, en realidad es difícil causar problemas de concurrencia. No puede acceder a componentes que no sean seguros para subprocesos ni al DOM. Debe pasar datos específicos dentro y fuera del hilo a través de objetos serializados. Entonces tienes que trabajar muy duro para crear problemas en tu código.

Política de seguridad de contenidos

Los trabajadores tienen su propio contexto de ejecución, que es diferente del contexto del documento que los creó. Por lo tanto, los Trabajadores no estarán sujetos a la política de seguridad de Contenido del documento. Por ejemplo, el documento está controlado por este encabezado http

Content-Security-Policy: script-src 'self'

 Esto evitará que todos los scripts de la página utilicen eval(). Pero si se crea un trabajador en el script, eval() aún se puede usar en el hilo del trabajador. Para controlar la Política de seguridad de contenido en el Trabajador, debe configurarla en el encabezado de respuesta http del script del Trabajador. Una excepción es que si el origen de su trabajador es un identificador único global (como blob://xxx), heredará la Política de seguridad de contenido del documento.

transferencia de datos

Los datos pasados ​​entre el hilo principal y el hilo de trabajo se copian en lugar de la dirección de memoria compartida. Los objetos se serializan antes de pasarse y luego se deserializan cuando se reciben. La mayoría de los navegadores implementan la copia mediante el algoritmo de clonación estructurada.

Adaptarse a la optimización interna del motor V8

Canal de compilación V8

  1. Prepare el entorno: V8 primero preparará el entorno de ejecución del código, que incluye espacio de pila y espacio de pila, contexto de ejecución global, alcance global, funciones integradas, funciones de extensión y objetos proporcionados por el entorno del host, y Sistema de bucle de mensajes. Inicialice el contexto de ejecución global y el alcance global. El contexto de ejecución incluye principalmente entorno variable, entorno léxico, this y cadena de alcance. Las variables declaradas por var y función se colocarán en el entorno de variables. Este paso se realiza antes de ejecutar el código, para que las variables puedan promocionarse. Las variables declaradas por const y let se colocarán en el entorno léxico, que es una estructura de pila. Cada vez que ingresa y sale del bloque de código {}, se empujarán y extraerán de la pila, y las que saldrán de la pila no será accesible, por lo que const y let tendrán efectos léxicos.
  2. Construya un sistema de bucle de eventos: el hilo principal necesita leer continuamente tareas de la cola de tareas para su ejecución, por lo que es necesario construir un mecanismo de eventos de bucle.
  3. Generar código de bytes: después de que V8 prepare el entorno de ejecución, primero realizará un análisis léxico y sintáctico (analizador) en el código y generará información de alcance y AST. Después de eso, la información de alcance y AST se ingresa a un intérprete llamado Ignition y convierte. en código de bytes. Bytecode es un código intermedio independiente de la plataforma. La ventaja de utilizar el código de bytes aquí es que se puede compilar en un código de máquina optimizado y el almacenamiento en caché del código de bytes ahorra mucha memoria que el almacenamiento en caché del código de máquina. El análisis se retrasará al generar el código de bytes. V8 no compilará todo el código a la vez. Si encuentra una declaración de función, no analizará inmediatamente el código dentro de la función. Solo generará AST y código de bytes de la función de nivel superior.
  4. Ejecutar código de bytes: el intérprete en V8 puede ejecutar código de bytes directamente. En el código de bytes, el código fuente se compila en Ldar, Agregar y otras instrucciones similares a ensambladores, que pueden implementar instrucciones como buscar, analizar instrucciones, ejecutar instrucciones y almacenar datos, etc. . Generalmente existen dos tipos de intérpretes: los basados ​​en pilas y los basados ​​en registros. Los intérpretes basados ​​en pilas utilizan pilas para guardar parámetros de funciones, resultados de cálculos intermedios, variables, etc. Las máquinas virtuales basadas en registros utilizan registros para guardar parámetros y resultados de cálculos intermedios. La mayoría de los intérpretes se basan en pilas, como la máquina virtual Java, la máquina virtual .Net y las primeras máquinas virtuales V8. La máquina virtual V8 actual adopta un diseño basado en registros.
  5. Compilación JIT justo a tiempo: aunque el código de bytes se puede ejecutar directamente, lleva mucho tiempo. Para mejorar la velocidad de ejecución del código, V8 agrega un monitor en el intérprete. Durante la ejecución del código de bytes, si un determinado fragmento de código se encuentra repetido. Si se ejecuta varias veces, el monitoreo marcará este código como código activo.

         Cuando un determinado fragmento de código se marca como código activo, V8 entregará el código de bytes al compilador de optimización TurboFan. El compilador de optimización compilará el código de bytes en código binario y luego realizará la compilación en el código binario compilado. Optimice la operación. y la eficiencia de ejecución del código de máquina binario optimizado mejorará enormemente. Si este código se ejecuta más tarde, V8 dará prioridad al código binario optimizado, este diseño se llama JIT (compilación justo a tiempo).

          Sin embargo, a diferencia de los lenguajes estáticos, JavaScript es un lenguaje dinámico flexible. Los tipos de variables y las propiedades de los objetos se pueden modificar en tiempo de ejecución. Sin embargo, el código optimizado por el compilador de optimización solo puede apuntar a tipos fijos. Una vez durante el proceso de ejecución, el Las variables se modifican dinámicamente, entonces el código de máquina optimizado se convertirá en código no válido. En este momento, el compilador de optimización necesita realizar operaciones de desoptimización y recurrirá al intérprete para su interpretación y ejecución la próxima vez que se ejecute. El proceso de desoptimización adicional es más lento que la ejecución directa convencional de código de bytes.

A partir del proceso de compilación anterior, podemos saber que js ejecuta repetidamente un fragmento del mismo código varias veces. Debido a la existencia de JIT, la velocidad es muy rápida (al mismo nivel que los lenguajes estáticamente fuertemente tipados como java y C#). Pero la premisa es que su tipo y estructura de objetos no se pueden cambiar a voluntad. Por ejemplo, el siguiente código.

const count = 10000;
let value = "";
for (let i = 0; i < count; i++) {
  value = i % 2 ? `${i}` : i;
  // do something...
}

Optimización de los objetos de almacenamiento del motor V8.

Los objetos JS se almacenan en el montón. Es más como un diccionario, con cadenas como nombres de clave. Cualquier objeto se puede utilizar como valor clave y el valor clave se puede leer y escribir a través del nombre de clave. Sin embargo, cuando V8 implementó el almacenamiento de objetos, no utilizó completamente el almacenamiento de diccionario, principalmente debido a consideraciones de rendimiento. Debido a que el diccionario es una estructura de datos no lineal, el cálculo de hash y los conflictos de hash hacen que la eficiencia de la consulta sea menor que la de las estructuras de datos almacenadas secuencialmente. Para mejorar la eficiencia del almacenamiento y la búsqueda, V8 adopta una estrategia de almacenamiento compleja. Las estructuras de almacenamiento secuencial son una pieza continua de memoria, como listas lineales y matrices, mientras que las estructuras no lineales generalmente ocupan memoria no contigua, como listas enlazadas y árboles.

El objeto se divide en propiedades regulares y propiedades de clasificación. Las propiedades numéricas se ordenan automáticamente en orden ascendente, lo que se denomina propiedades de clasificación, y se colocan al principio de todas las propiedades del objeto. Las propiedades de cadena se colocan dentro de propiedades normales en el orden en que se crean.

En V8, para mejorar eficazmente el rendimiento de almacenamiento y acceso a estas dos propiedades, se utilizan dos estructuras de datos lineales para almacenar propiedades de clasificación y propiedades regulares, respectivamente, es decir, las dos propiedades ocultas de elementos y propiedades.

Cuando se cumplen estas dos condiciones: no se agregan nuevos atributos después de crear el objeto; no se eliminan atributos después de crear el objeto, V8 creará una clase oculta para cada objeto y habrá un valor de atributo de mapa en el objeto que apunta a él. La clase oculta de un objeto registra cierta información básica de diseño del objeto, incluidos los dos puntos siguientes: todos los atributos contenidos en el objeto y el desplazamiento de cada valor de atributo en relación con la memoria inicial del objeto. De esta forma, no es necesario realizar una serie de procesos al leer atributos, puede obtener directamente el desplazamiento y calcular la dirección de memoria.

但 js 是动态语言,对象属性是可以被改变的。给一个对象添加新属性,删除属性,或者改变某个属性的数据类型都会改变这个对象的形状,从而使 V8 重新构建新的隐藏类,降低性能。

所以非必要不推荐使用 delete 关键字删除对象的属性或添加/修改属性,最好在对象声明时就确定。同时声明相同的对象字面量时最好保证完全相同:

// 不好,x、y顺序不同
const object1 = { a: 1, b: 2 };
const object2 = { b: 1, a: 2 };

// 好
const object1 = { a: 1, b: 2 };
const object2 = { a: 1, b: 2 };

 第一种写法两个对象的形状不同,会生成不同的隐藏类,无法复用。

当多次重复读取同一个对象的属性时,V8 会为它建立内联缓存(Inline Cache)。例如这段代码:

const object = { a: 1, b: 2 };

const read = (object) => object.a;

for (let i = 0; i < 1000; i++) {
  read(object);
}

正常读取对象属性的流程是:查找隐藏类 -> 查找内存偏移量 -> 得到属性值。当读取操作多次执行时,V8 会优化这个流程。

内联缓存简称 IC。在 V8 执行函数的过程中,会观察函数中一些调用点 (CallSite) 上的关键的中间数据,然后将这些数据缓存起来,当下次再次执行该函数的时候,V8 就可以直接利用这些中间数据,节省了再次获取这些数据的过程,因此 V8 利用 IC,可以有效提升一些重复代码的执行效率。

内联缓存会为每个函数维护一个 反馈向量 (FeedBack Vector)。反馈向量由很多项组成的,每一项称为一个插槽 (Slot),上面的代码中,V8 会依次将执行 read 函数的中间数据写入到反馈向量的插槽中。

代码中 return object.a 是一个调用点,因为它读取了对象属性,那么 V8 会在 read 函数的反馈向量中为这个调用点分配一个插槽,每个插槽中包括了插槽的索引 (slot index)、插槽的类型 (type)、插槽的状态 (state)、隐藏类 (map) 的地址、还有属性的偏移量,当 V8 再次调用 read 函数执行到 return object.a 时,它会在对应的插槽中查找 a 属性的偏移量,之后 V8 就能直接去内存中获取 object.a 的属性值了,相比去隐藏类中查找有更快的执行效率。

const object1 = { a: 1, b: 2 };
const object2 = { a: 3, b: 4 };

const read = (object) => object.a;

for (let i = 0; i < 1000; i++) {
  read(object1);
  read(object2);
}

 Si el código se vuelve así, encontraremos que las formas de los dos objetos leídos en cada bucle son diferentes, por lo que sus clases ocultas también son diferentes. Cuando V8 lee el segundo objeto, encontrará que la clase oculta en la ranura es diferente de la que se está leyendo, por lo que agregará una nueva clase oculta y un desplazamiento de memoria de valor de atributo a la ranura. En este momento, habrá dos clases ocultas y compensaciones en el espacio. Cada vez que se leen las propiedades de un objeto, V8 las compara una por una. Si la clase oculta del objeto que se lee es la misma que una de las clases ocultas en la ranura, entonces se utiliza el desplazamiento de la clase oculta acertada. Si no hay equivalente, la nueva información también se agrega a ese espacio.

  • Si una ranura contiene sólo 1 clase oculta, este estado se llama monomórfico ( monomorphic);

  • Si una ranura contiene de 2 a 4 clases ocultas, este estado se llama polimorfismo ( polymorphic);

  • Si hay más de 4 clases ocultas en un espacio, este estado se denomina superestado ( magamorphic).

Se puede ver que el rendimiento del monomorfismo es el mejor, por lo que podemos intentar evitar modificar objetos o leer múltiples objetos en una función que se ejecuta varias veces para lograr un mejor rendimiento.

Esto puede llevar a una cosa. Cuando miré la versión React17 antes, la explicación oficial sobre "Por qué usar _jsx en lugar de createElement" hablaba de algunas deficiencias de createElement. Mencionaba que createElement es "altamente polimórfico" y difícil de optimizar desde el principio. Nivel V8. De hecho, si comprende este artículo, comprenderá lo que significa esa oración. En realidad, es el superestado mencionado en el artículo, por lo que comprenderá por qué el funcionario dijo que createElement es difícil de optimizar. La función createElement se llamará muchas veces en la página, pero los accesorios del componente y otros parámetros que acepta son diferentes, por lo que se generarán muchos cachés en línea, por lo que se dice que es "altamente polimórfico (hiperestado)". (Pero al menos este point_jsx todavía parece no haberse resuelto, pero aún es muy poderoso que puedan darse cuenta de esto)


¡Por fin se acabó! No es fácil de hacer, indíquelo al reimprimir. ¡Dale el visto bueno! ! !

Supongo que te gusta

Origin blog.csdn.net/YN2000609/article/details/132403663
Recomendado
Clasificación