프런트 엔드 성능 최적화 (2023 간단하고 이해하기 쉽고 자세한 설명)

      우리는 프론트 엔드 작업이나 인터뷰에서 성능 최적화라는 단어를 자주 접하는데, 이것은 결국 누구나 이야기할 수 있는 이야기이기 때문에 어렵지 않은 것 같습니다. 하지만 직장의 다양한 시나리오에서 성능 병목 현상이 발생할 때 직접적인 성능 솔루션을 원하거나 인터뷰 중에 면접관에게 좋은 인상을 주고 싶다면 "생각나는 대로 말하세요" 또는 "대략적인 답변을 드리는 것"에만 매달릴 수는 없습니다. 아이디어를 얻기 위해서는 모든 각도에서 체계적이고 심층적인 지식 지도가 필요합니다. "성능 최적화"는 단순한 "최적화"가 아니기 때문에 이 기사는 내 개인적인 프런트 엔드 지식의 요약이라고 볼 수도 있습니다. 무엇을 의미합니까? 최적화 계획을 실행하기 전에 먼저 이러한 방식으로 최적화해야 하는 이유와 그렇게 하는 목적이 무엇인지 알아야 합니다. 이를 위해서는 프레임워크, js, css, 브라우저, js 엔진, 네트워크 등의 원리를 잘 이해해야 합니다. 따라서 성능 최적화에는 실제로 너무 많은 프런트엔드 지식, 심지어 대부분의 프런트엔드 지식이 포함됩니다.

      먼저 프론트엔드 성능의 본질에 대해 말씀드리자면, 프론트엔드는 네트워크 애플리케이션입니다. 애플리케이션의 성능은 운영 효율성에 따라 결정됩니다. 앞서 네트워크를 추가하면 네트워크 효율성과 관련이 있습니다. 그래서 프론트엔드 성능의 핵심은 네트워크 성능과 운영성능이라고 생각합니다. 따라서 프런트엔드 성능 최적화 시스템의 두 가지 주요 범주는 네트워크와 런타임입니다. 그런 다음 이 두 가지 주요 테마에서 각 작은 영역을 세분화하여 거대한 프런트엔드 지식 그래프를 엮기에 충분합니다.

네트워크 수준

      네트워크 연결을 수도관에 비유하면 지금 페이지를 열고자 하면 상대방의 손에 물컵이 들려 있고, 그 물을 자신의 컵에 연결하려는 것으로 볼 수 있습니다. 더 빨리 가고 싶다면 3가지 방법이 있습니다: 1. 수도관의 흐름을 더 크고 빠르게 만듭니다, 2. 반대쪽은 컵의 물을 줄이도록 합니다, 3. 내 컵에 물이 있는데 물을 주지 않습니다. 당신이 필요합니다. 수도관 트래픽은 네트워크 대역폭, 프로토콜 최적화 및 네트워크 속도에 영향을 미치는 기타 요소입니다. 물 한 컵이 줄어들면 압축, 코드 분할, 지연 로딩 및 요청을 줄이기 위한 기타 수단을 의미하며 마지막은 캐싱을 사용하는 것입니다.

      먼저 네트워크 속도에 대해 이야기하자면, 네트워크 속도는 사용자의 운영자에 의해서만 결정되는 것이 아니라 네트워크 프로토콜의 원리를 숙지하고 효율성을 최적화하기 위해 네트워크 프로토콜을 조정함으로써 결정됩니다.

      컴퓨터 네트워크는 이론적으로는 OSI 7계층 모델이지만 실제로는 물리 계층, 데이터 링크 계층, 네트워크 계층, 전송 계층, 애플리케이션 계층 등 5개 계층(또는 4개 계층 모델)으로 볼 수 있습니다. 각 계층은 자체 프로토콜을 캡슐화, 해체 및 구문 분석하고 자체 작업을 수행하는 역할을 담당합니다. 예를 들면 궁녀가 황제에게 옷을 한 겹 입히고 벗는 것과 같으니, 당신은 외투를 담당하고 나는 속옷을 담당하며 각자의 본분을 다하는 것입니다. 프론트 엔드로서 우리는 매일 다루는 애플리케이션 계층 HTTP 프로토콜을 시작으로 주로 애플리케이션 계층과 전송 계층에 중점을 둡니다.

http 프로토콜 최적화

1. HTTP/1.1에서는 동일한 도메인 이름에 대한 브라우저 요청의 최대 동시 요청 제한(Chrome의 경우 일반적으로 6)에 도달하지 않도록 해야 합니다.

  • 페이지 리소스 요청 수가 많은 경우 여러 도메인 이름을 준비하고 서로 다른 도메인 이름 요청을 사용하여 최대 동시성 제한을 우회할 수 있습니다.
  • 여러 개의 작은 아이콘을 하나의 큰 이미지로 병합할 수 있으므로 여러 이미지 리소스에는 단 한 번의 요청만 필요합니다. 프런트 엔드는 CSS의 배경 위치 스타일을 통해 해당 아이콘(스프라이트 이미지라고도 함)을 표시합니다.

2. HTTP 헤더 크기 줄이기

  • 예를 들어 동일한 도메인의 요청은 자동으로 쿠키를 전달하므로 인증이 필요하지 않으면 낭비입니다. 이러한 종류의 리소스는 사이트와 동일한 도메인에 있어서는 안 됩니다.

3. HTTP 캐시를 최대한 활용하세요. 캐싱은 요청을 직접적으로 제거하고 네트워크 성능을 크게 향상시킬 수 있습니다.

  • 브라우저는 no-cache 및 캐시 제어의 max-stale과 같은 HTTP 헤더 값을 사용하여 강력한 캐싱 사용 여부, 캐싱 협상, 캐시 만료 사용 가능 여부 및 기타 기능을 제어할 수 있습니다.
  • 서버는 캐시 제어의 max-age, public, stale-while-revalidate와 같은 http 헤더 값을 사용하여 강력한 캐시 시간, 프록시 서버에서 캐시할 수 있는지 여부, 캐시가 만료되는 기간 및 방법을 제어합니다. 캐시를 자동으로 새로 고치는 데 시간이 오래 걸립니다.

4. HTTP/2.0 이상으로 업그레이드하면 네트워크 성능이 크게 향상될 수 있습니다. (TLS를 사용해야 합니다. 즉, https)

5. HTTPS 최적화

       HTTPS 성능을 많이 소모하는 두 가지 주요 측면은 다음과 같습니다.

  • 첫 번째 단계는 TLS 프로토콜 핸드셰이크 프로세스입니다.
  • 두 번째 단계는 핸드셰이크 후에 대칭적으로 암호화된 메시지를 전송하는 것입니다.

두 번째 단계에서는 현재 주류인 대칭 암호화 알고리즘인 AES와 ChaCha20의 성능이 좋으며 일부 CPU 제조업체에서도 이를 위해 하드웨어 수준 최적화를 수행했기 때문에 이 단계에서 암호화 성능 소비는 매우 적다고 할 수 있습니다.

첫 번째 단계에서 TLS 프로토콜 핸드셰이크 프로세스는 네트워크 지연을 증가시킬 뿐만 아니라(최대 2 RTT 네트워크 왕복 시간이 소요될 수 있음) 핸드셰이크 프로세스의 일부 단계에서는 다음과 같은 성능 손실도 발생합니다.

      ECDHE 키 계약 알고리즘을 사용하는 경우 클라이언트와 서버 모두 핸드셰이크 프로세스 중에 타원 곡선 공개 및 개인 키를 일시적으로 생성해야 합니다. 클라이언트가 인증서를 확인할 때 CA 서버에 액세스하여 CRL 또는 OCSP를 얻습니다. 서버의 인증서가 폐기되었는지 확인하기 위해 양측은 대칭 암호화 키인 Pre-Master를 계산합니다. 전체 TLS 프로토콜 핸드셰이크의 어느 단계에 있는지 더 잘 이해하려면 다음 그림을 참조하세요.

TLS 핸드셰이크

HTTPS는 다음 방법을 사용하여 최적화할 수 있습니다.

  • 하드웨어 최적화: 서버는 AES-NI 명령어 세트를 지원하는 CPU를 사용합니다.
  • 소프트웨어 최적화: Linux 버전 및 TLS 버전을 업그레이드합니다. TLS/1.3은 1 RTT 시간만 필요하도록 핸드셰이크 수를 크게 최적화했으며 순방향 보안을 지원합니다(즉, 현재 또는 미래에 키가 크랙되더라도 이전에 가로채는 메시지의 보안에는 영향을 미치지 않음을 의미).
  • 인증서 최적화: OCSP 스테이플링. 정상적인 상황에서 브라우저는 인증서가 해지되었는지 여부를 CA에 확인해야 하며, 서버는 주기적으로 CA에 인증서 상태를 쿼리하고 타임스탬프 및 서명이 포함된 응답 결과를 얻고 이를 캐시할 수 있습니다. 클라이언트가 연결 요청을 시작하면 서버는 TLS 핸드셰이크 프로세스 중에 "응답 결과"를 브라우저에 직접 전송하므로 브라우저가 CA 자체를 요청할 필요가 없습니다.
  • 세션 재사용 1: 세션 ID. 양측 모두 메모리에 세션을 유지합니다. 다음에 연결이 설정될 때 hello 메시지는 세션 ID를 전달합니다. 서버는 세션 ID를 수신한 후 메모리에서 검색합니다. 발견하면 , 세션 키를 직접 사용하여 복원합니다. 세션 상태, 나머지 프로세스를 건너뜁니다. 보안을 위해 메모리의 세션 키는 주기적으로 만료됩니다. 그러나 두 가지 단점이 있습니다: 1. 서버는 각 클라이언트의 세션 키를 저장해야 하며 클라이언트 수가 증가하면 서버의 메모리 사용량이 증가합니다. 2. 요즘 웹 사이트 서비스는 일반적으로 로드 밸런싱을 통해 여러 서버에서 제공되므로 클라이언트가 다시 연결하면 지난번에 방문했던 서버에 연결되지 않을 수 있습니다.서버에 연결되지 않으면 여전히 전체 과정을 거쳐야 합니다. TLS 핸드셰이크 프로세스.
  • 세션 재사용 2: 세션 티켓, 클라이언트와 서버가 처음으로 연결을 설정할 때 서버는 "세션 키"를 암호화하여 클라이언트에 티켓으로 보내고 클라이언트는 티켓을 저장합니다. 이는 웹 개발에서 사용자 신원을 확인하는 데 사용되는 토큰 체계와 유사합니다. 클라이언트가 서버에 다시 연결되면 클라이언트는 티켓을 보내게 되는데, 서버가 이를 복호화할 수 있으면 마지막 세션 키를 획득한 후 유효 기간을 확인하고, 문제가 없으면 세션을 복원하고 암호화된 통신이 직접 시작됩니다. 서버만이 이 키를 암호화하고 복호화할 수 있기 때문에 복호화가 가능하다면 사기가 발생하지 않는다는 의미입니다. 클러스터 서버의 경우 각 서버에서 "세션 키"를 암호화하는 데 사용되는 키가 일관성이 있는지 확인하십시오. 그러면 클라이언트가 서버에 액세스하기 위해 티켓을 가져올 때 세션이 복원될 수 있습니다.

세션 ID나 세션 티켓에는 순방향 보안이 없습니다. "세션 키"를 암호화하는 키가 크랙되거나 서버가 키를 유출하면 이전에 하이재킹된 통신 암호문이 크랙될 수 있기 때문입니다. 동시에 재생 공격도 직면하기 어렵습니다. 소위 재생 공격은 중개인이 게시 요청 메시지를 가로챈다고 가정하지만 그 안에 있는 정보를 해독할 수는 없지만 비멱등성 메시지를 재사용하여 서버를 요청하세요.https를 바로 재사용할 수 있는 티켓서버가 있기 때문이죠. 재생 공격의 피해를 줄이기 위해 암호화된 세션 키에 대해 합리적인 만료 시간을 설정할 수 있습니다.


다음은 http 지식 포인트에 대한 자세한 소개입니다.

HTTP/0.9

초기 버전은 매우 간단하며 빠른 사용 촉진을 목적으로 합니다. 기능은 단순한 get html입니다. 요청 메시지 형식은 다음과 같습니다.

GET /index.html

 HTTP/1.0

인터넷의 발달과 함께 http는 더 많은 기능을 충족해야 하므로 친숙한 http 헤더, 상태 코드, GET POST HEAD 요청 방식, 캐시 등을 갖췄습니다. 사진, 동영상 등의 바이너리 파일도 전송할 수 있습니다.

이 버전의 단점은 각 요청 후에 TCP 연결이 끊어지고 다음 http 요청에서 연결을 다시 설정하기 위해 TCP가 필요하다는 것입니다. 따라서 일부 브라우저는 비표준 Connection: keep-alive 헤더를 추가했으며 서버는 동일한 헤더로 응답합니다. 이 계약을 통해 TCP는 긴 연결을 유지할 수 있습니다. 후속 http 요청은 한쪽 당사자가 적극적으로 종료할 때까지 이 TCP를 재사용할 수 있습니다. 그것.        

HTTP/1.1

현재 널리 사용되는 버전은 1.1인데, 이 버전에서는 기본적으로 tcp long 연결을 사용하므로, 종료하려면 Connection:close 헤더를 적극적으로 추가해야 합니다.

또한 파이프라인 메커니즘(파이프라인)도 있어 클라이언트는 http 반환을 기다리지 않고 동일한 TCP 연결에서 여러 http 요청을 지속적으로 보낼 수 있습니다. 과거에는 TCP 연결에서 한 번에 하나의 HTTP 요청만 보낼 수 있도록 HTTP 요청을 설계했고, 그 반환 값을 받은 후에야 HTTP 요청이 완료되고 다음 HTTP 요청을 보낼 수 있었습니다. http/1.1 버전은 파이프라인 메커니즘을 기반으로 여러 https를 연속적으로 보낼 수 있지만 1.1은 여전히 ​​서버에서 FIFO(선입선출) 순서로만 응답을 반환할 수 있으므로 응답 중에 첫 번째 http가 매우 느리면 후속 항목은 여전히 ​​첫 번째 http에 의해 차단됩니다. 여러 개의 연속 응답을 받으면 브라우저는 이를 Content-Length로 나눕니다.

또한, 청크 전송 인코딩이 추가되어 버퍼 형식을 스트림 스트림으로 대체합니다. 예를 들어 비디오의 경우 더 이상 메모리에 완전히 읽어들인 다음 보낼 필요가 없으며 스트림을 사용하여 각 작은 부분을 읽은 후 작은 부분을 보낼 수 있습니다. Transfer-Encoding: 청크 헤더를 사용하여 켜십시오. 각 청크 앞에는 청크 길이를 나타내는 16진수 숫자가 있습니다. 숫자가 0이면 청크가 전송되었음을 의미합니다. 대용량 파일 전송이나 파일 처리와 같은 시나리오에서 이 기능을 사용하면 효율성이 향상되고 메모리 사용량이 줄어들 수 있습니다.

이 버전에는 다음과 같은 단점이 있습니다.

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 연결

버전 2.0은 http 아래에 바이너리 프레이밍 레이어를 추가하는 것으로 생각할 수 있습니다. 메시지(완전한 요청 또는 응답을 메시지라고 함)는 여러 프레임으로 나누어지며 프레임에는 유형, 길이, 플래그, 스트림 식별자 스트림 및 페이로드 프레임 페이로드가 포함됩니다. 동시에 스트림이라는 추상적인 개념도 추가됩니다. 각 프레임의 스트림 식별자는 해당 프레임이 속한 스트림을 나타냅니다. http/2.0은 기다리지 않고 순서대로 전송될 수 있으므로 송신자/수신자는 순서에 따라 전송됩니다. 스트림 식별자에 데이터가 조립됩니다. 양쪽 끝의 스트림 ID 중복으로 인한 충돌을 방지하기 위해 클라이언트가 시작한 스트림의 ID는 홀수, 서버가 시작한 스트림의 ID는 짝수입니다. 원본 프로토콜의 내용은 영향을 받지 않으며 http1.1의 첫 번째 정보 헤더는 헤더 프레임에 캡슐화되고 요청 본문은 데이터 프레임에 캡슐화됩니다. 여러 요청은 하나의 TCP 채널만 사용합니다. 이 이니셔티브는 실제로 새 페이지 로드가 HTTP/1.1에 비해 11.81% ~ 47.7% 가속화될 수 있음을 보여주었습니다. http/2.0에서는 다중 도메인 이름, 스프라이트 이미지 등의 최적화 방법이 더 이상 필요하지 않습니다.

HPACK 알고리즘

HPACK 알고리즘은 HTTP/2에 새로 도입된 알고리즘으로 HTTP 헤더를 압축하는 데 사용됩니다. 원칙은 다음과 같습니다.

RFC 7541의 부록 A에 따르면 클라이언트와 서버는 공통 헤더 이름과 공통 헤더 이름 및 값의 조합에 대한 코드가 포함된 공통 정적 사전(정적 테이블)을 유지 관리합니다. 클라이언트와 서버는 첫 번째 항목을 따릅니다
. 원칙은 콘텐츠를 동적으로 추가할 수 있는 공통 동적 사전(동적 테이블)을 유지하며,
클라이언트와 서버는 RFC 7541의 부록 B에 따라 이 정적 허프만 코드 테이블을 기반으로 허프만 코딩을 지원합니다.

서버 푸시     

과거에는 브라우저가 서버 데이터를 얻기 위해 요청을 적극적으로 시작해야 했습니다. 이를 위해서는 웹사이트에 추가 js 요청 스크립트를 추가해야 하며, 호출하기 전에 js 리소스가 로드될 때까지 기다려야 합니다. 이로 인해 요청 시간이 지연되고 요청이 많아집니다. HTTP/2는 브라우저가 적극적으로 요청을 보낼 필요가 없는 서버 측 활성 푸시를 지원하여 요청 효율성을 절약하고 개발 경험을 최적화합니다. 프런트 엔드는 EventSource를 통해 서버에서 푸시 이벤트를 수신할 수 있습니다.

HTTP/3.0

HTTP/2.0은 멀티플렉싱, 헤더 압축 등 이전 버전에 비해 많은 최적화를 이루었지만 기본 레이어가 TCP를 기반으로 하기 때문에 일부 문제점을 해결하기 어렵습니다.

헤드 오브 라인 차단

HTTP는 TCP 위에서 실행됩니다. 바이너리 프레이밍은 이미 HTTP 수준의 여러 요청이 차단되지 않도록 보장할 수 있지만 위에서 언급한 TCP 원칙을 통해 TCP에도 HOL(head-of-line) 차단 및 재전송 기능이 있다는 것을 알 수 있습니다. 패키지 승인은 반환되지 않으며 후속 패키지도 전송되지 않습니다. 따라서 HTTP/2.0은 HTTP 수준의 HOL 차단만 해결하고 여전히 전체 네트워크 링크에서 차단됩니다. 현대 네트워크 환경에서 더 빠르게 전송하기 위해 새로운 프로토콜을 사용할 수 있다면 좋을 것입니다.

TCP, TLS 핸드셰이크 대기 시간

TCP에는 3개의 핸드셰이크가 있고, TLS(1.2)에는 4개의 핸드셰이크가 있으며, 실제 http 요청을 발행하려면 총 3번의 RTT 지연이 필요합니다. 동시에 TCP의 혼잡 회피 메커니즘은 느린 시작에서 시작되므로 속도가 더욱 느려집니다.

네트워크를 전환하면 다시 연결됩니다.

우리는 TCP 연결의 고유성이 양쪽 끝의 IP와 포트에 따라 결정된다는 것을 알고 있습니다. 요즘은 모바일 네트워크와 교통수단이 발달해 사무실에 들어가거나 집에 가면 휴대폰이 자동으로 WIFI에 연결된다. 초. 모두 IP 변경을 유발하여 이전 TCP 연결을 무효화합니다. 나타나는 현상은 반쯤 열려 있던 웹 페이지가 갑자기 로드되지 않고, 중간에 버퍼링된 비디오가 마지막에 버퍼링되지 않는다는 것입니다.

QUIC 프로토콜

위의 문제는 TCP 고유의 문제이므로 이를 해결하려면 프로토콜을 변경하면 됩니다. http/3.0에서는 QUIC 프로토콜을 사용합니다. 완전히 새로운 프로토콜에는 하드웨어 지원이 필요하며 대중화하는 데 필연적으로 오랜 시간이 걸립니다. 따라서 QUIC는 기존 프로토콜 UDP를 기반으로 구축되었습니다.

QUIC 프로토콜에는 다음과 같은 많은 장점이 있습니다.

헤드 오브 라인 차단 없음


QUIC 프로토콜은 HTTP/2와 유사한 스트림 및 다중화 개념을 가지고 있으며 동일한 연결에서 여러 스트림을 동시에 전송할 수도 있습니다. 스트림은 HTTP 요청으로 간주될 수 있습니다.

QUIC에서 사용하는 전송 프로토콜은 UDP이므로 UDP는 패킷의 순서를 신경 쓰지 않으며 패킷이 손실된 경우 UDP도 신경 쓰지 않습니다.

그러나 QUIC 프로토콜은 여전히 ​​데이터 패킷의 신뢰성을 보장해야 하며, 각 데이터 패킷은 시퀀스 번호로 고유하게 식별됩니다. Flow의 패킷이 손실되면 Flow의 다른 패킷이 도착하더라도 HTTP/3에서 데이터를 읽을 수 없으며 QUIC가 손실된 패킷을 재전송할 때까지 데이터가 HTTP/3으로 전달되지 않습니다.

특정 흐름의 데이터 패킷이 완전히 수신되는 한 HTTP/3는 이 흐름의 데이터를 읽을 수 있습니다. 이는 한 스트림에서 패킷이 손실되면 다른 스트림이 영향을 받는 HTTP/2와 다릅니다.

따라서 QUIC 연결에서는 여러 스트림 사이에 종속성이 없으며 모두 독립적입니다.특정 스트림에서 패킷이 손실되면 해당 스트림에만 영향을 미치고 다른 스트림에는 영향을 미치지 않습니다.

더 빠른 연결 설정

HTTP/1과 HTTP/2 프로토콜의 경우 TCP와 TLS는 계층화되어 각각 커널이 구현하는 전송 계층과 OpenSSL 라이브러리가 구현하는 프리젠테이션 계층에 속하므로 병합이 어렵고 흔들릴 필요가 있습니다. 일괄적으로 먼저 TCP 핸드셰이크를 수행한 다음 TLS 핸드셰이크를 수행합니다.

HTTP/3도 데이터를 전송하기 전에 QUIC 프로토콜 핸드셰이크가 필요하지만 이 핸드셰이크 프로세스에는 1 RTT만 필요합니다. 핸드셰이크의 목적은 연결 마이그레이션과 같은 양 당사자의 "연결 ID"를 확인하는 것입니다(예: 네트워크 필요) IP 전환으로 인해 마이그레이션됨) 연결 ID를 기준으로 구현됩니다.

HTTP/3의 QUIC 프로토콜은 TLS와 계층화되지 않지만 QUIC는 내부적으로 TLS를 포함합니다. 자체 프레임에서 TLS의 "기록"을 전달합니다. 또한 QUIC는 TLS 1.3을 사용하므로 하나의 RTT만 연결 설정을 완료할 수 있습니다. 두 번째 연결 중에도 애플리케이션 데이터 패킷을 QUIC 핸드셰이크 정보(연결 정보 + TLS 정보)와 함께 전송하여 0-RTT 효과를 얻을 수 있습니다.

아래 그림의 오른쪽 부분에 표시된 것처럼 HTTP/3 세션이 복원되면 페이로드 데이터가 첫 번째 패킷과 함께 전송되어 0-RTT를 달성할 수 있습니다.

연결 마이그레이션

모바일 기기의 네트워크가 4G에서 WiFi로 전환되면 IP 주소가 변경되었음을 의미하므로 연결을 끊었다가 다시 설정해야 합니다. 연결을 설정하는 과정에는 TCP 3방향 핸드셰이크 지연과 TLS 4방향 핸드셰이크 그리고 TCP 슬로우 스타트의 감속 과정은 사용자에게 네트워크가 갑자기 정체된 듯한 느낌을 주기 때문에 연결 마이그레이션 비용이 매우 높습니다. 고속 열차를 타고 있는 경우 IP hUI가 지속적으로 변경되어 TCP 연결이 지속적으로 다시 연결될 수 있습니다.

QUIC 프로토콜은 연결을 "바인딩"하기 위해 4-튜플 방법을 사용하지 않지만 연결 ID를 사용하여 통신의 두 끝점을 표시합니다. 클라이언트와 서버는 각각 자신을 표시할 ID 세트를 선택할 수 있습니다. 모바일 장치 네트워크 변경 후 IP 주소가 변경되는 경우 컨텍스트 정보(예: 연결 ID, TLS 키 등)가 계속 유지되는 한 원래 연결을 "원활하게" 재사용할 수 있어 비용이 절감됩니다. 지연 없이 재접속이 가능하도록 연결 마이그레이션 기능을 제공합니다.

단순화된 프레임 구조, QPACK 최적화된 헤더 압축

HTTP/3은 HTTP/2와 동일한 바이너리 프레임 구조를 사용하는데, 차이점은 Stream을 HTTP/2의 바이너리 프레임에 정의해야 하는 반면, HTTP/3 자체는 더 이상 Stream을 정의할 필요가 없고 QUIC에서 Stream을 사용한다는 점입니다. HTTP/ 3의 프레임 구조도 더 단순해졌습니다.

HTTP/3 프레임

  다양한 프레임 유형에 따라 일반적으로 데이터 프레임과 제어 프레임의 두 가지 범주로 구분됩니다. 헤더 프레임(HTTP 헤더)과 데이터 프레임(HTTP 패킷 본문)은 데이터 프레임에 속합니다.

HTTP/3은 헤더 압축 알고리즘 측면에서도 QPACK으로 업그레이드되었습니다. HTTP/2의 HPACK 인코딩 방법과 유사하게 HTTP/3의 QPACK도 정적 테이블, 동적 테이블 및 허프만 인코딩을 사용합니다.

정적 테이블의 변경 사항과 관련하여 HTTP/2의 HPACK 정적 테이블에는 항목이 61개만 있는 반면, HTTP/3의 QPACK 정적 테이블은 91개 항목으로 확장되었습니다.

HTTP/2와 HTTP/3의 허프만 인코딩은 크게 다르지 않지만 동적 테이블 인코딩 및 디코딩 방법은 다릅니다.

소위 동적 테이블은 첫 번째 요청-응답 이후에 양쪽 당사자가 정적 테이블에 포함되지 않은 헤더 항목(예: 일부 사용자 정의 헤더)을 각자의 동적 테이블로 업데이트한 다음 1개의 숫자만 사용하여 이를 나타냅니다. 그러면 상대방은 매번 긴 데이터를 전송할 필요 없이 이 숫자를 기반으로 동적 테이블에서 해당 데이터를 조회할 수 있으므로 코딩 효율성이 크게 향상됩니다.

동적 테이블은 순차적임을 알 수 있는데, 첫 번째 요청 헤더가 손실되고 후속 요청에서 이 헤더가 다시 발생하면 발신자는 상대방이 이미 동적 테이블에 해당 헤더를 저장했다고 생각하여 헤더를 압축합니다. 그러나 상대방은 동적 테이블을 구축하지 않았기 때문에 이 HPACK 헤더를 디코딩할 수 없으므로, 정상적인 디코딩이 이루어지기 전에 첫 번째 요청에서 손실된 데이터 패킷이 재전송될 때까지 후속 요청의 디코딩을 차단해야 합니다.

HTTP/3의 QPACK은 이 문제를 해결하지만 어떻게 해결합니까?

QUIC에는 두 개의 특별한 단방향 스트림이 있습니다. 소위 단방향 스트림의 한쪽 끝만 메시지를 보낼 수 있습니다. 양방향 스트림은 HTTP 메시지를 전송하는 데 사용됩니다. 이 두 단방향 스트림의 사용법은 다음과 같습니다.

하나는 QPACK Encoder Stream이라 불리는데 사전(Key-Value)을 상대방에게 전달하는데 사용되는데, 예를 들어 정적 테이블에 속하지 않는 HTTP 요청 헤더에 직면했을 때 클라이언트는 이를 통해 사전을 보낼 수 있습니다. 스트림; 다른 하나는 QPACK 디코더 스트림으로, 상대방에게 응답하고 방금 보낸 사전이 로컬 동적 테이블로 업데이트되었음을 ​​알리고 나중에 인코딩에 이 사전을 사용할 수 있음을 알리는 데 사용됩니다. 이 두 가지 특별한 단방향 스트림은 양측의 동적 테이블을 동기화하는 데 사용됩니다. 인코딩 당사자는 디코딩 당사자로부터 업데이트 확인 알림을 받은 후 동적 테이블을 사용하여 HTTP 헤더를 인코딩합니다. 동적 테이블의 업데이트 메시지가 손실되면 일부 헤더만 압축되지 않고 HTTP 요청이 차단되지 않습니다.

HTTP 캐싱에 대한 자세한 설명

네트워크 리소스를 요청할 필요가 없고 로컬 캐시에서 직접 얻는 경우 당연히 가장 빠릅니다. 캐시 메커니즘은 http 프로토콜에 정의되어 있으며 로컬 캐시(강한 캐시라고도 함)와 요청을 통해 확인해야 하는 캐시(협상 캐시라고도 함)로 구분됩니다.

로컬 캐시(강력한 캐시)

http1.0에서는 반환 값의 만료 시간을 나타내기 위해 만료 응답 헤더를 사용하는데, 이 시간 내에 브라우저는 재요청 없이 바로 캐시를 사용할 수 있다. http1.1 이후에는 더 많은 캐싱 요구 사항을 충족할 수 있는 Cache-Control 응답 헤더로 변경되었으며, 내부의 max-age는 요청 후 N초 후에 리소스가 만료됨을 나타냅니다. max-age는 브라우저가 응답을 받은 후 경과한 시간이 아니라 원본 서버에서 응답이 생성된 후 경과한 시간으로 브라우저 시간과는 아무런 관련이 없습니다. 따라서 네트워크의 다른 캐싱 서버가 100초 동안 응답을 저장하는 경우(응답 헤더 필드 Age를 사용하여 표시됨) 브라우저 캐시는 만료 시간에서 100초를 공제합니다. 캐시가 만료되면(재검증 중 오래된 것, 최대 오래된 것 등의 영향을 무시함) 브라우저는 리소스가 업데이트되었는지 확인하기 위한 조건부 요청(협상 캐시라고도 함)을 시작합니다.

조건부 요청(캐시 협상)

요청 헤더에는 각각 마지막 요청 응답 헤더의 Last-Modified 및 etag인 If-Modified-Since 및 If-None-Match 필드가 있습니다. Last-Modified는 리소스가 마지막으로 수정된 시간(초)을 나타냅니다. Etag는 리소스의 특정 버전에 대한 식별자입니다(예: 콘텐츠를 해싱하여 etag를 생성할 수 있음). If-None-Match 또는 If-Modified-Since에 변경 사항이 없으면 서버는 304 상태 코드 응답을 반환하고, 브라우저는 리소스가 업데이트되지 않은 것으로 간주하여 로컬 캐시를 재사용합니다. Last-Modified 레코드의 수정 시간은 초 단위이므로 수정 빈도가 1초 이내에 발생하면 업데이트 여부를 정확하게 판단할 수 없으므로 Last-Modified보다 etag의 판단 우선순위가 높습니다.

Cache-Control에 no-cache를 설정하면 강제로 Strong Caching을 사용하지 않게 되며, Negotiation Caching을 직접 사용 즉, max-age=0으로 사용하게 됩니다. no-store가 설정되면 캐시가 사용되지 않습니다.

요청에 대한 브라우저의 캐싱 전략은 간단합니다. 캐싱은 응답 헤더와 요청 헤더에 의해 결정되는 것을 볼 수 있습니다. 개발 과정에서 게이트웨이와 브라우저는 일반적으로 이를 자동으로 설정합니다. 특정 요구 사항이 있는 경우 , 더 많은 캐시 제어 기능을 사용하도록 사용자 정의할 수 있습니다.

완전한 캐시 제어 기능

Cache-Control에는 더 자세한 캐시 제어 기능도 있습니다. 응답 헤더와 요청 헤더의 전체 의미는 아래 표를 참조하세요.

응답 헤더

요청 헤더(응답 헤더에 포함되지 않은 헤더만 나열됨) |max-stale|캐시는 max-stale 초 이내에 만료될 때에도 계속 사용할 수 있습니다. | |min-fresh|새 캐시를 반환하려면 캐시 서비스가 필요합니다. 그렇지 않으면 로컬 캐시 사용 | |only-if-cached| 브라우저는 캐시 서버가 캐시한 경우에만 대상 리소스를 반환하도록 요구합니다 |

TCP 프로토콜 최적화

노드를 작성할 때 필요할 수 있습니다. 괜찮아요 걱정하지 마세요 순수 프론트엔드에만 관심있는 분들은 건너뛰셔도 됩니다 :)

먼저 다양한 문제에 대한 최적화 방법을 직접 제시하고 구체적인 TCP 원리와 이러한 현상이 발생하는 이유에 대해서는 나중에 자세히 소개하겠습니다.

다음 TCP 최적화는 일반적으로 요청 측에서 발생합니다.

1. 첫 번째 요청의 크기는 14kb를 초과하지 않아야 하며 이는 TCP의 느린 시작을 효과적으로 활용할 수 있으며 프런트 엔드 페이지의 첫 번째 패키지에도 동일하게 수행할 수 있습니다.

  • 초기 TCP 윈도우가 10이고 MSS가 1460이라고 가정하면 첫 번째 요청의 리소스 크기는 14600바이트(약 14kb)를 초과해서는 안 됩니다. 이 방법을 사용하면 상대방의 tcp를 한 번에 보낼 수 있지만 그렇지 않으면 최소 2번에 걸쳐 전송하게 되므로 추가 RTT(네트워크 왕복 시간)가 필요합니다.

2. 작은 데이터 패킷(MSS 이하)을 자주 전송하여 TCP가 차단되면 어떻게 해야 합니까?
이는 게임 작업(tcp 프로토콜은 일반적으로 사용되지 않지만) 및 명령줄 SSH에서 매우 일반적입니다.

  • Nagel의 알고리즘 끄기
  • 지연된 응답 방지

TCP 패킷 손실 재전송을 최적화하는 방법

  • net.ipv4.tcp_sack을 통해 SACK 켜기(기본적으로 활성화됨)
  • net.ipv4.tcp_dsack을 통해 D-SACK 켜기(기본적으로 활성화됨)

다음 TCP 최적화는 일반적으로 서버 측에서 발생합니다.

1. 서버가 수신하는 동시 요청 수가 너무 높거나 SYN 공격을 받아 SYN 대기열이 가득 차서 요청에 응답할 수 없습니다.

  • 사용 syn 쿠키
  • 동기화 재시도 횟수 줄이기
  • 싱크 큐 크기 늘리기

2. TIME-WAIT가 너무 많으면 사용 가능한 포트가 가득 차서 더 이상 요청을 보낼 수 없습니다.

  • 운영 체제의 tcp_max_tw_buckets 구성을 사용하여 동시 TIME-WAIT 수를 제어합니다.
  • 가능하다면 클라이언트 또는 서버의 포트 범위와 IP 주소를 늘리십시오.

위의 TCP 최적화 방법은 TCP 메커니즘을 이해하고 운영 체제 매개변수를 조정하는 것을 기반으로 하며, 이를 통해 어느 정도 네트워크 성능 최적화를 달성할 수 있습니다. 아래에서는 tcp의 구현 메커니즘부터 시작하여 이러한 최적화 방법이 수행하는 작업을 설명합니다.

TCP 전송 전에 반드시 연결이 이루어져야 한다는 것은 우리 모두 알고 있지만 실제로 네트워크 전송에는 연결 설정이 필요하지 않습니다. 원래 네트워크는 버스티(bursty)하고 언제든지 전송하도록 설계되었기 때문에 전화망 설계는 포기되었습니다. . 일반적으로 소위 TCP 연결은 실제로 서로 간의 일부 통신을 저장하는 두 장치 간의 상태일 뿐이며 실제 연결이 아닙니다. TCP는 5개의 튜플을 통해 동일한 연결인지 구별해야 하는데, 그 중 하나는 프로토콜이고 나머지 4개는 src_ip, src_port, dst_ip, dst_port(이중 엔드 IP 및 포트 번호)입니다. 또한 tcp 메시지 세그먼트의 헤더에는 네 가지 중요한 사항이 있습니다. 시퀀스 번호는 패킷의 시퀀스 번호(seq)로 전체 데이터 스트림에서 이 패킷의 데이터 부분의 첫 번째 비트 위치를 나타냅니다. , 네트워크 패킷 혼란을 해결하는 데 사용됩니다. 순서 문제. Acknowledgement Number(ack)는 이번에 받은 데이터의 길이 + 이번에 받은 seq를 의미하며, 수신을 확인하고 패킷이 손실되지 않는 문제를 해결하기 위해 사용되는 상대방(송신자)의 다음 시퀀스 번호이기도 합니다. . Advertised-Window라고도 하는 Window는 흐름 제어를 구현하는 데 사용되는 슬라이딩 창입니다. TCP 플래그는 SYN, FIN, ACK 등과 같은 패킷 유형으로 주로 TCP 상태 기계를 제어하는 ​​데 사용됩니다.

주요 부분은 아래와 같이 소개됩니다.

tcp 세 번 "핸드셰이크"

3방향 핸드셰이크의 핵심은 양 당사자의 초기 시퀀스 번호, MSS, 창 및 기타 정보를 알아야 순서가 잘못된 상황에서 데이터를 순서대로 연결할 수 있으며 최대 네트워크와 하드웨어의 운반 능력을 확인할 수 있습니다.

ISN(Initial Seq Sequence Number)은 32비트로 4마이크로초 주기로 연속적으로 1을 더해 가상클럭에서 생성되며, 2^32를 초과하면 0으로 돌아가고 한 주기는 4.55시간이 소요된다. 각 연결 설정이 0부터 시작되지 않는 이유는 연결이 끊어졌다가 다시 설정된 후 늦게 도착하는 새 패킷과 이전 패킷 간의 순서 충돌 문제를 방지하기 위한 것입니다. 4.55시간이 MSL(최대 세그먼트 수명)을 초과했으며 이전 패키지가 더 이상 존재하지 않습니다.

  • 클라이언트는 초기 seq가 x라고 가정하여 SYN(플래그: SYN) 패킷을 보냅니다. 따라서 seq = x입니다. 클라이언트 TCP가 SYN_SEND 상태로 들어갑니다.
  • 서버 tcp는 초기에 LISTEN 상태이며 이를 수신한 후 ACK 패킷(플래그: ACK, SYN)을 보냅니다. 초기 seq는 y, seq = y, ack = x + 1이라고 가정합니다. SYN은 1개의 길이를 차지하므로 다음 클라이언트는 x + 1부터 시작해야 합니다. 서버가 SYN_RECEIVED 상태로 들어갑니다.
  • 클라이언트는 ACK 패킷을 수신한 후 seq = x + 1, ack = y + 1로 보냅니다. 그런 다음 PSH 패킷의 실제 콘텐츠(데이터 길이가 100이라고 가정)를 계속해서 보냅니다. seq = x + 1, ack = y + 1. seq와 ack의 실제 내용이 ack 패킷과 변하지 않는 이유는 Flag가 ACK이기 때문에 확인용으로만 사용되며 길이 자체를 차지하지 않기 때문입니다. 클라이언트는 ESTABLISHED 상태로 들어갑니다.
  • 서버는 ACK 패킷을 수신한 후 seq = y + 1, ack = x + 101로 보냅니다. 서버가 ESTABLISHED 상태로 들어갑니다.

seq 및 ack 계산은 이 패킷 캡처 사진과 비교할 수 있습니다(사진은 인터넷에서 가져온 것이며, 그 안의 시퀀스 번호는 상대 시퀀스 번호입니다).

TCP 전송 프로세스 전송에 실패했습니다. 이미지 파일을 직접 업로드하는 것이 좋습니다.

SYN 시간 초과 및 공격

3방향 핸드셰이크 동안 서버가 SYN 패킷을 수신하고 SYN-ACK를 반환한 후 TCP는 중간 연결 상태에 있습니다. 운영 체제 커널은 일시적으로 연결을 SYN 대기열에 넣습니다. 핸드쉐이크가 성공하면 연결은 완전한 대기열(연결 대기열)에 추가됩니다. 서버가 클라이언트로부터 ACK를 받지 못하면 타임아웃되어 재시도합니다. 기본 재시도는 5회이며, 1초, 1초, 2초, 4초... 다섯 번째 타임아웃까지 두 배로 증가하여 총 5번의 시간이 소요됩니다. 63초가 지나면 tcp 연결이 끊어집니다. 이 연결을 끊습니다. 일부 공격자는 이 기능을 이용하여 서버에 대량의 SYN 패킷을 보낸 다음 연결을 끊습니다. 서버는 SYN 대기열에서 연결을 지우기 전에 63초를 기다려야 하므로 서버 TCP의 SYN 대기열이 가득 찼습니다. 계속해서 서비스를 제공할 수 없습니다. 이 상황은 일반적인 대규모 동시성 조건에서도 발생할 수 있습니다. 현재 Linux에서는 다음 매개변수를 설정할 수 있습니다.

  • tcp_syncookies를 사용하면 4중 정보, 64초마다 증가하는 타임스탬프, SYN 대기열이 가득 찬 후의 MSS 옵션 값에서 특수 시퀀스 번호(쿠키라고도 함)를 생성할 수 있으며, 이 쿠키는 seq로 클라이언트에 직접 전송될 수 있습니다. Jianlian. 이러한 영리한 방식으로 tcp_syncookies는 일부 정보를 로컬에 저장할 필요 없이 SYN에 저장합니다. 주의 깊은 시청자라면 tcp_syncookies가 연결을 설정하기 위해 두 번의 핸드셰이크만 필요한 것처럼 보인다는 사실을 알게 될 것입니다. 이를 tcp 표준에 통합하는 것은 어떨까요? 단점도 있기 때문에 1. MSS의 인코딩은 3비트에 불과하므로 최대 8개의 MSS 값만 사용할 수 있습니다. 2. 서버는 SYN 및 SYN+ ACK에서만 협상되는 클라이언트의 SYN 메시지에서 다른 옵션을 거부해야 합니다. 서버에는 Wscale 및 SACK와 같은 옵션을 저장할 공간이 없기 때문입니다. 3. 암호화 작업을 추가했습니다. 따라서 대규모 일반 동시성으로 인해 SYN 큐가 가득 찬 경우 이 방법을 사용하지 마십시오. 이는 단지 tcp의 쇠약화된 버전일 뿐입니다.
  • tcp_synack_retries를 사용하면 SYN-ACK 시간 초과에 대한 재시도 횟수를 줄이고 SYN 대기열 정리 시간도 단축할 수 있습니다.
  • tcp_max_syn_backlog는 최대 SYN 연결 수를 늘립니다. 즉, SYN 대기열을 늘립니다.
  • tcp_abort_on_overflow, SYN 대기열이 가득 차면 연결을 거부합니다.

tcp "wave"를 네 번

클라이언트가 먼저 연결을 끊는다고 가정하면 예제의 seq는 마지막 핸드셰이크를 따릅니다.

닫기 전에 양쪽 끝의 tcp 상태는 ESTABLISHED입니다.

  1. 클라이언트는 닫을 수 있음을 나타내기 위해 FIN 패킷(플래그: FIN)을 보냅니다(seq = x + 101, ack = y + 1). 클라이언트가 FIN-WAIT-1 상태로 변경됩니다.
  2. 서버는 이 FIN을 수신하고 ACK, seq = y + 1, ack = x + 102를 반환합니다. 서버가 CLOSE-WAIT 상태로 변경됩니다. 이 ACK를 받은 후 클라이언트는 FIN-WAIT-2 상태로 변경됩니다.
  3. 서버에는 완료되지 않은 작업이 있을 수 있으며 완료 후 종료를 결정하기 위해 FIN 패킷을 보냅니다(seq = y + 1, ack = x + 102). 서버가 LAST-ACK 상태로 변경됩니다.
  4. 클라이언트는 FIN, seq = x + 102, ack = y + 2를 받은 후 확인 ACK를 반환합니다. 클라이언트가 TIME-WAIT 상태로 변경됩니다.
  5. 서버는 클라이언트로부터 ACK를 받은 후 바로 연결을 종료하고 CLOSED 상태로 변경됩니다. 클라이언트는 2*MSL 시간 동안 기다린 후 다시 서버로부터 FIN을 수신하지 못하면 연결을 닫고 CLOSED 상태로 변경됩니다.

긴 TIME-WAIT가 필요한 이유는 무엇입니까? 1. 지연된 오래된 패킷을 수신하여 4-튜플을 재사용하는 새로운 연결을 피할 수 있습니다.. 2. 서버가 닫혔는지 확인할 수 있습니다.

TIME-WAIT 시간이 2*MSL(최대 세그먼트 생존 시간, RFC793에서는 MSL을 2분으로 정의하고 Linux에서는 30초로 설정함)인 이유는 무엇입니까? 왜냐하면 FIN을 보낸 후 ACK 대기 시간이 초과되면 서버가 재전송하기 때문입니다. FIN의 생존 MSL 시간이 가장 길고 이 전에 재전송이 이루어져야 하며, 재전송된 FIN의 MSL 생존 시간도 가장 길기 때문입니다. 따라서 MSL 시간의 2배 후에도 클라이언트는 여전히 서버로부터 재전송을 받지 못했습니다. 이는 서버가 ACK를 수신하고 닫혔음을 의미하므로 클라이언트를 닫을 수 있습니다.

연결 끊김으로 인해 TIME-WAIT가 너무 많이 발생하면 어떻게 해야 합니까?

우리는 Linux가 연결을 닫기 전에 기본적으로 1분을 기다린다는 것을 알고 있으며, 이 때 포트는 항상 점유되어 있습니다. 동시에 짧은 연결이 많은 경우 TIME-WAIT가 너무 많으면 포트가 가득 차거나 CPU가 너무 많이 점유될 수 있습니다.

마지막 두 구성은 사용하지 않는 것이 좋습니다.

  • tcp_max_tw_buckets는 동시 TIME-WAIT 횟수를 제어하며, 기본값은 180000이며, 이를 초과할 경우 시스템이 파기하고 로그를 기록한다.
  • ip_local_port_range, 클라이언트 포트 범위 늘리기
  • 가능하다면 서버의 서비스 포트를 늘려주세요. (tcp 연결은 ip와 포트를 기반으로 하며, 많을수록 더 많은 연결이 가능합니다.)
  • 가능하다면 클라이언트 또는 서버 IP를 늘리십시오.
  • tcp_tw_reuse, 타임스탬프를 사용하려면 먼저 클라이언트와 서버 모두에서 활성화해야 하며 클라이언트에만 적용됩니다. 열린 후에는 TIME-WAIT를 기다릴 필요가 없으며 1초만 소요됩니다. 새로운 연결은 이 소켓을 직접 재사용할 수 있습니다. 타임스탬프를 활성화해야 하는 이유는 무엇입니까? 이전 연결의 패킷이 돌아서 최종적으로 서버에 도달할 수 있고, 소켓을 재사용하는 새로운 연결 5중은 이전 패킷과 동일하기 때문에 타임스탬프가 새 패킷보다 이전인 한 패킷이어야 합니다. 피할 수 있는 오래된 연결 쓸모없는 오래된 패키지가 실수로 받아들여졌습니다.
  • tcp_tw_recycle, tcp_tw_recycle 처리가 더 공격적이므로 TIME_WAIT 상태에서 소켓을 빠르게 재활용합니다. 빠른 재활용은 tcp_timestamps와 tcp_tw_recycle이 모두 활성화된 경우에만 발생합니다. 클라이언트가 NAT 환경을 통해 서버에 접속할 때 서버가 적극적으로 닫힌 후 TIME_WAIT 상태가 생성되며, 서버에서 tcp_timestamps 및 tcp_tw_recycle 옵션이 모두 활성화된 경우 동일한 소스 IP 호스트에서 TCP 분할 시간은 60분 이내입니다. 초 스탬프는 증가해야 하며, 그렇지 않으면 폐기됩니다. Linux는 4.12 커널 버전부터 tcp_tw_recycle 구성을 제거했습니다.

TCP 슬라이딩 윈도우 및 흐름 제어

운영 체제는 tcp에 대한 캐시 영역을 열어 tcp가 보내고 받는 최대 데이터 패킷 수를 제한하며 이는 슬라이딩 창으로 시각화할 수 있습니다. 송신자의 창을 송신 창 swnd라고 하고, 수신자의 창을 수신 창 rwnd라고 합니다. 전송되었지만 수신되지 않은 데이터의 길이 + 전송될 버퍼링된 데이터의 길이 = 전송 창의 총 길이. 

보내기 창

핸드셰이크 동안 양쪽 끝은 창 값을 교환하며 결국 최소값이 사용됩니다. 발신자의 창 크기가 20이고 처음에 10개의 패킷을 보냈지만 아직 ack를 받지 못했기 때문에 앞으로는 10개의 패킷만 더 버퍼에 넣을 수 있다고 가정합니다. 버퍼가 가득 차면 더 이상 데이터를 보낼 수 없습니다. 수신자는 데이터를 수신할 때 이를 버퍼에 넣습니다. 처리 능력이 상대방의 전송 능력보다 낮으면 버퍼가 쌓이고 수신 가능한 창은 작아집니다. ack가 전달하는 창 값은 송신자가 허용할 것입니다. 전송되는 데이터의 양을 줄이기 위해.. 그리고 운영체제에서도 버퍼의 크기를 조정하게 되는데 이때 원래 사용 가능한 수신창이 10인데 ack를 통해 상대방에게 알렸는데 운영체제가 갑자기 버퍼를 축소시키는 상황이 발생할 수 있다. 그리고 창은 15로 줄어듭니다. 대신에 사용 가능한 수신 창은 15로 줄어듭니다. 나는 5를 빚지고 있습니다. 발신자는 이전에 사용 가능한 창이 10이라는 것을 수신했으므로 데이터는 계속 전송되지만 수신자가 데이터를 처리할 수 없어 시간이 초과됩니다. 이러한 상황을 피하기 위해 TCP는 운영 체제가 버퍼를 수정하려는 경우 수정된 사용 가능한 창을 미리 보내야 한다고 강제합니다.

위의 내용을 통해 TCP는 양쪽 창을 통한 전송 트래픽을 제한한다는 것을 알 수 있으며, 창이 0이면 전송을 일시적으로 중지해야 한다는 의미입니다. 수신자의 버퍼가 가득 차서 윈도우가 0인 ack가 전송되고, 수신자가 수신할 수 있는 기간이 지난 후 윈도우가 0이 아닌 ack가 전송자에게 계속 전송하도록 알리기 위해 전송됩니다. 만약 이 Ack가 분실된다면 매우 심각한 상황이 될 것이며 송신자는 수신자가 이를 수신할 수 있을지 전혀 모르고 계속 기다리다가 교착상태에 빠지게 될 것입니다. 이 문제를 피하기 위해 TCP의 설계는 보낸 사람에게 전송을 중지하라는 알림을 받은 후(즉, 창 0의 응답을 받은 후) 타이머를 시작하고 30분마다 창 프로브(Window Probe)를 보내는 것입니다. -60초 메시지를 받은 후 수신자는 현재 창에 응답해야 합니다. 창 감지가 세 번 연속 0이면 일부 TCP 구현에서는 RST 패킷을 보내 연결을 중단합니다.

수신자의 창이 이미 매우 작다면 발신자는 여전히 이 창을 사용하여 데이터를 보낼 것입니다. tcp 헤더 + ip 헤더는 40바이트이고 데이터는 몇 바이트에 불과하므로 매우 비경제적입니다. 이 상황을 피하는 방법은 무엇입니까? 십진 드라마 패키지를 최적화하는 방법을 살펴보겠습니다.        

TCP 작은 패킷

수신자의 경우 작은 창에서 전송이 허용되지 않는 한 수신자는 일반적으로 다음 전략을 사용합니다. 수신 창이 MSS 및 캐시 공간/2의 최소값보다 작으면 피어에게 창이 다음과 같다고 알립니다. 0이고 데이터 전송을 중지합니다. 창이 해당 조건보다 커질 때까지.

보낸 사람의 경우 Nagle 알고리즘을 사용하면 보내기 전에 다음 두 조건 중 하나만 충족됩니다.

  • 창 크기 >= MSS 및 총 데이터 크기 >= MSS
  • 이전에 보낸 데이터에 대한 응답을 받습니다.

그 중 어느 것도 충족되지 않으면 계속해서 데이터를 축적했다가 특정 조건이 충족되면 한꺼번에 전송합니다.

의사 코드는 다음과 같습니다

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이 매우 좋지 않으므로 꺼야 합니다. (Nagle 알고리즘은 전역 시스템 구성이 없으며 각 응용 프로그램에 따라 꺼야 함)

스몰 데이터 최적화에 대해 이야기한 후 이제 슬라이딩 윈도우에 대해 이야기해 보겠습니다. 실제로 tcp가 최종적으로 채택한 윈도우는 슬라이딩 윈도우에 의해 완전히 결정되지 않습니다. 슬라이딩 윈도우는 양쪽 끝이 송수신 기능을 초과하는 것을 방지할 뿐입니다. 네트워크 양쪽 끝 사이의 조건도 고려해야 합니다. 양쪽 끝이 보내고 받는 경우 성능은 매우 좋지만 현재 네트워크 환경이 매우 열악합니다. 많은 양의 데이터를 보내면 네트워크가 더 혼잡해질 뿐이므로 문제가 발생할 수 있습니다. 여전히 혼잡 창입니다. TCP는 슬라이딩 창과 혼잡 창의 최소값을 사용합니다.

TCP 느린 시작 및 혼잡 회피

먼저 MSS가 무엇인지부터 이야기하자면, MSS는 tcp 세그먼트에 허용되는 최대 데이터 바이트 길이, 즉 MTU(하드웨어에서 지정한 데이터 링크 계층의 최대 데이터 길이)에서 IP 헤더를 뺀 값 20바이트를 뺀 것입니다. tcp 헤더 20 바이트 단위로 계산되며 일반적으로 1460입니다. 이는 TCP 패킷이 최대 1460바이트의 상위 계층 데이터를 전달할 수 있음을 의미합니다. TCP 핸드셰이크 중에 양쪽 끝에서 최소 MSS가 협상됩니다. 실제 네트워크 환경에서는 요청이 수많은 중간 장치를 거치게 되고, 이에 의해 SYN의 MSS가 수정되며, 결국 양쪽 끝의 최소값이 아닌 전체 경로의 최소값이 됩니다.

TCP에는 네트워크 정체를 방지하는 역할을 하는 cwnd(congestion window)가 있는데, 그 값은 TCP가 한 번에 보낼 수 있는 패킷 수를 나타내는 TCP 세그먼트 크기의 정수배입니다(편의상 1부터 시작합니다). 초기 값은 매우 작으며, 사용 가능한 네트워크 전송 리소스를 감지하기 위해 패킷 손실 및 재전송이 발생할 때까지 점차 증가합니다. 전통적인 느린 시작 알고리즘의 빠른 승인 모드에서는 확인 승인이 성공적으로 수신될 때마다 cwnd + 1이므로 느린 시작 임계값 ssthresh가 다음과 같을 때까지 cwnd는 1, 2, 4, 8, 16... 도달했습니다.(느린 시작 임계값), ssthresh는 일반적으로 max(외부 데이터 값/2, 2*SMSS)와 동일하며, SMSS는 보낸 사람의 최대 세그먼트 크기입니다. cwnd < ssthresh이면 느린 시작 알고리즘이 사용되며, cwnd >= ssthresh이면 혼잡 회피 알고리즘이 사용됩니다.

혼잡 회피 알고리즘은 각 확인 응답이 수신된 후 cwnd가 1/cwnd만큼 증가합니다. 즉, 마지막으로 전송된 모든 패킷이 확인됩니다(cwnd + 1). 느린 시작 알고리즘과 달리 혼잡 회피 알고리즘은 두 가지 유형의 재전송이 발생할 때까지 선형적으로 증가한 다음 감소합니다. 1. 타임아웃 재전송이 발생하고, 2. 빠른 재전송이 발생합니다.

빠른/지연된 응답, 타임아웃 재전송 및 빠른 재전송

Fast Acknowledgement 모드에서는 수신자가 패킷을 수신한 후 즉시 확인 Ack를 보내지만, TCP는 패킷을 수신할 때마다 확인 Ack를 반환하지 않으므로 네트워크 대역폭이 낭비됩니다. TCP도 지연 승인 모드로 진입할 수 있으며, 수신측에서는 지연 승인 타이머를 시작하여 200ms마다 ack를 보낼지 여부를 확인하고, 전송할 데이터가 있으면 ack와 병합할 수도 있습니다. 발신자가 한 번에 여러 패킷을 보낸다고 가정하면 피어는 10개의 ack로 응답하지 않고 수신된 가장 큰 연속 패킷의 마지막 ack로만 응답합니다. 예를 들어 1, 2, 3,...10이 전송되면 수신측에서는 이를 모두 수신하므로 10이라는 ack으로 응답하므로 송신측에서는 처음 10개를 모두 수신했다는 것을 알 수 있고 다음은 11시부터 시작됩니다. 중간에 패킷 손실이 있는 경우 패킷 손실 이전의 ack를 반환합니다.

타임아웃 재전송: 송신자는 전송 후 타이머를 시작합니다. 타임아웃 시간(RTO)은 1 RTT(패킷 왕복 시간)보다 약간 크게 설정하는 것이 적합합니다. 수신 확인 시간이 초과되면 데이터 패킷이 재전송됩니다. 재전송된 데이터가 타임아웃된 경우 타임아웃이 두 ​​배로 늘어납니다. 이때, ssthresh는 cwnd/2가 되고, cwnd는 초기값으로 재설정되며, Slow Start 알고리즘이 사용됩니다. cwnd가 절벽에서 떨어지는 것을 볼 수 있으므로, 타임아웃 재전송 발생은 네트워크 성능에 큰 영향을 미칩니다. 재전송하기 전에 RTO를 기다려야 합니까?

Fast retransmission(빠른 재전송): TCP는 빠른 재전송 설계를 가지고 있습니다. 수신자가 패킷을 순서대로 수신하지 못하면 가장 큰 연속 ack로 응답하고, 송신자가 3개의 ack를 연속으로 수신하면 패킷이 손실된 것으로 간주하여 신속하게 응답할 수 있습니다. 느린 시작으로 돌아가지 않고 해당 패킷을 한 번 재전송합니다. 예를 들어 수신자는 1, 2, 4를 수신했으므로 2의 ACK로 응답한 다음 5와 6을 수신했습니다. 3이 중간에 중단되었으므로 여전히 2의 ACK로 두 번 응답했습니다. 보낸 사람은 동일한 ack를 세 번 연속으로 받았기 때문에 3개가 손실되었음을 알고 3개를 빠르게 재전송했습니다. 수신자는 3을 수신하고 데이터는 연속적이므로 6의 ack가 반환되고 송신자는 7부터 계속 전송할 수 있습니다. 아래 그림과 같습니다.

빠른 재전송이 발생하는 경우:

  1. ssthresh = cwnd/2, cwnd = ssthresh + 3, 손실된 패킷 재전송을 시작하고 빠른 복구 알고리즘에 들어갑니다. +3의 이유는 3개의 중복 ack가 수신되었기 때문입니다. 이는 현재 네트워크가 적어도 정상적으로 이 3개의 추가 패킷을 보내고 받을 수 있음을 나타냅니다.
  2. 중복된 ACK를 수신하면 혼잡 윈도우가 1만큼 증가합니다.
  3. 새로운 데이터 패킷의 ACK가 수신되면 첫 번째 단계에서 cwnd가 ssthresh 값으로 설정됩니다.

빠른 재전송 알고리즘은 Tahoe 버전 4.3BSD에서 처음 등장했고, 빠른 복구는 Reno 버전 4.3BSD에서 처음 등장했으며 TCP 혼잡 제어 알고리즘의 Reno 버전이라고도 합니다. Reno의 고속 재전송 알고리즘은 하나의 패킷의 재전송을 목표로 하고 있음을 알 수 있으나, 실제로는 재전송 타임아웃으로 인해 많은 데이터 패킷의 재전송이 발생할 수 있으므로 하나의 데이터 윈도우에서 다수의 데이터 패킷이 손실되는 경우 고속일 때 문제가 발생한다. 재전송 및 빠른 복구 알고리즘이 트리거됩니다. 따라서 NewReno가 등장하는데, Reno의 빠른 복구를 기반으로 약간 수정되어 한 창 내에서 여러 패킷 손실을 복구할 수 있습니다. 구체적으로 Reno는 새로운 데이터에 대한 ACK를 받으면 빠른 복구 상태를 종료하고, NewReno는 빠른 복구 상태를 종료하기 전에 해당 창의 모든 데이터 패킷으로부터 확인을 받아야 하므로 처리량이 더욱 향상됩니다.

TCP를 "정확하게" 재전송하는 방법

부분적인 패킷 손실이 발생하면 보낸 사람은 어떤 패킷이 부분적으로 또는 완전히 손실되었는지 알 수 없습니다. 예를 들어, 수신 측에서 1, 2, 4, 5, 6을 수신하면 송신 측에서는 3 이후의 패킷이 손실되었음을 ack를 통해 알 수 있으며 빠른 재전송을 트리거할 수 있습니다. 이때 두 가지 결정이 내려집니다. 1. 세 번째 패킷만 재전송합니다. 2. 4번, 5번, 6번 패킷...도 손실되었는지 모르므로 3번 패킷 이후는 모두 재전송합니다. 두 가지 옵션 모두 별로 좋지 않습니다. 3개만 다시 보내면 나중에 정말 잃어버리면 각각 재전송을 기다려야 합니다. 그런데 모두 직접 재전송하게 되면 3개만 손실되면 낭비가 되는데, 어떻게 최적화해야 할까요?

빠른 재전송은 시간 초과 재전송이 발생할 가능성을 줄일 뿐이며, 빠른 재전송이나 시간 초과 재전송은 둘 중 하나 또는 전부를 재전송할지 여부를 정확하게 아는 문제를 해결하지 못합니다. SACK(Selective Acknowledgement)라는 더 나은 방법이 있는데, 이는 양쪽 끝에서 모두 지원되어야 합니다. Linux는 net.ipv4.tcp_sack 매개변수를 통해 이 방법을 전환합니다. SACK은 tcp 헤더에 데이터 조각을 추가하여 보낸 사람에게 최대 연속 데이터 세그먼트 외에 어떤 데이터 세그먼트가 수신되었는지 알려주므로 보낸 사람은 데이터를 재전송할 필요가 없다는 것을 알 수 있습니다. 천 마디 말보다 한 장의 사진이 더 중요합니다.

另外还有Duplicate SACK(D-SACK)。如果接收方的确认 ACK 丢包了,发送发会误以为接收方没收到,触发超时重传,这时接收方会收到重复数据。或者由于发送包遇到网络拥堵了,重传的包比之前的包更早到达,接收方也会收到重复数据。这时可以在 tcp 头里加一段SACK数据,值是重复的数据段范围,因为数据段小于 ack,发送端就知道这些数据接收方已经收过了,不会再重传。

 D-SACK在 Linux 中通过net.ipv4.tcp_dsack参数开关。

总结一下SACK和D-SACK的作用就是:让发送方知道哪些包没收到、是否重复收包,可以判断出是数据包丢了、还是 ack 丢了、还是数据包被网络延迟了、还是网络中把数据包复制了。

更『厉害』的缓存:Service Worker

上面讲到的 HTTP 缓存控制权主要还是在后端,而且如果缓存过期了,虽然有协商缓存,但多多少少还是有一点请求的,这就要求必须有网络,同时它一般只能缓存 get 请求。这些限制使前端做不了像客户端那样的本地应用。那么有没有什么办法能让前端彻底的代理缓存,无论是静态资源还是 api 接口通通都可以由前端自己来决定,甚至可以把网页像 App 一样变成一个彻底的本地应用。这就是接下来要讲到的Service Worker,让我们看看它有哪些特性。

离线缓存

Service Worker可以看作是应用与网络请求之间的代理,它可以拦截请求,基于网络是否可用或者其他自定义逻辑来采取合适的行为。举个例子,你可以在应用第一次打开后,将 html、css、js、图片等资源都缓存起来,下一次打开网页时拦截请求并直接返回缓存,这样你的应用离线也可以打开了。如果后来设备联网了,你可以在后台请求最新资源并判断是否更新了,如果更新了你可以提醒用户刷新升级。在启动上,使用 Service Worker 的前端应用完全可以不需要网络,就像客户端 App 一样。

推送通知

요청을 프록시하는 것 외에도 서비스 워커는 앱 알림과 마찬가지로 브라우저가 알림을 보내도록 적극적으로 허용할 수도 있습니다. 이 기능을 사용하여 "사용자 리콜", "핫 알림" 등을 수행할 수 있습니다.

금지 품목

기본 js 코드는 렌더링 스레드에서 실행되고 서비스 워커는 다른 작업자 스레드에서 실행되므로 기본 스레드를 차단하지는 않지만 운영 DOM과 같은 일부 API를 사용할 수 없게 됩니다. 동시에 완전 비동기식으로 설계되어 있어 XHR, Web Storage 등 동기식 API를 사용할 수 없으며, fetch 요청을 사용할 수 있습니다. 동적 import()도 불가능하며 정적 가져오기 모듈만 가능합니다.

보안상의 이유로 Service Worker는 HTTPS 프로토콜에서만 실행될 수 있습니다.(http를 허용하려면 localhost를 사용하십시오.) 결국 요청을 인수하는 능력은 이미 매우 강력합니다. 중개자가 악의적으로 변조하는 경우 일반 사용자가 이를 수행할 수 있습니다. . 웹페이지는 결코 올바른 콘텐츠를 렌더링하지 않습니다. FireFox에서는 시크릿 모드에서도 사용할 수 없습니다.

지침

서비스 워커 코드는 독립적인 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이고 Service Worker의 js가 https://www.xxx.com/js/service-worker.js 및 /js/service-worker에 준비되어 있다고 가정합니다. .js는 실제로 요청은 https://www.xxx.com/js/service-worker.js입니다. 구성의 범위는 서비스 워커가 적용되는 경로를 나타냅니다. 범위를 설정하지 않으면 기본 루트 디렉터리가 적용됩니다. 서비스 워커는 웹 페이지의 모든 경로에서 사용됩니다. 예제의 작성 방법에 따라 ./를 설정한 경우 유효 경로는 /js/*이고 ../가 루트 디렉터리입니다.

서비스 워커는 이 3가지 수명 주기를 거칩니다.

  1. 다운로드
  2. 설치하다
  3. 활성화

첫 번째는 다운로드 단계입니다. 서비스워커가 관리하는 웹페이지에 들어가면 바로 다운로드가 시작됩니다. 이전에 다운로드한 적이 있는 경우 이번 다운로드 후에 업데이트가 결정될 수 있으며, 다음과 같은 상황에서 업데이트가 결정됩니다.

  1. 범위 내에서 페이지 이동이 발생했습니다.
  2. 서비스 워커에서 이벤트가 트리거되었으며 24시간 이내에 다운로드되지 않았습니다.

다운로드한 파일이 새로운 파일인 것으로 확인되면 설치를 시도하며, 새 파일인지 판단하는 기준은 먼저 다운로드한 후 이전 파일과 바이트 단위로 비교하는 것입니다.

서비스 워커를 처음 사용하는 경우 설치를 시도한 후 성공적으로 설치되면 활성화합니다.

기존의 Service Worker가 이미 사용 중이라면 백그라운드에 설치되어 설치 후 활성화되지 않는 상황을 Worker in Waiting이라고 합니다. 이전 js와 새 js가 논리적 충돌이 있을 수 있다고 상상해 보십시오. 이전 js가 한동안 실행되어 왔으며 이전 js를 새 j로 직접 교체하고 웹 페이지를 계속 실행하면 직접적으로 충돌이 발생할 수 있습니다.

새로운 서비스 워커는 언제 활성화되나요? 새 서비스 워커가 활성 워커가 되기 전에 이전 서비스 워커를 사용하는 모든 페이지가 닫힐 때까지 기다려야 합니다. ServiceWorkerGlobalScope.skipWaiting()을 사용하여 대기를 직접 건너뛸 수도 있습니다. Clients.claim()을 사용하면 새 서비스 워커가 현재 존재하는 페이지(이전 서비스 워커를 사용하는 페이지)를 제어할 수 있습니다.

이벤트를 수신하여 설치 또는 활성화가 언제 발생하는지 알 수 있습니다. 가장 일반적으로 사용되는 이벤트는 페이지가 요청을 시작할 때 트리거되는 FetchEvent입니다. 또한 Cache를 사용하여 데이터를 캐시하고 FetchEvent.respondWith()를 사용하여 요청을 반환할 수 있습니다. 원하는 반환 값. 다음은 캐시 요청을 작성하는 일반적인 방법입니다.

// 缓存版本,可以升级版本让过去的缓存失效
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)
  );
});

위의 코드 캐시는 설정된 후에는 업데이트되지 않습니다. 콘텐츠가 변경될 수 있고 캐시가 오래되지 않을까 걱정되는 경우 먼저 캐시로 돌아가서 사용자가 최대한 빨리 콘텐츠를 볼 수 있도록 할 수 있으며, 그런 다음 서비스 워커 백그라운드에서 최신 정보를 요청합니다. 데이터가 캐시로 업데이트되고, 마지막으로 메인 스레드에 알림이 전달되어 사용자에게 콘텐츠가 업데이트되었음을 ​​알리므로 사용자는 애플리케이션을 업그레이드할지 여부를 결정할 수 있습니다. 백그라운드 요청 및 업데이트 판단을 위한 코드를 직접 작성해 볼 수 있습니다. 여기서는 서비스 워커가 요청한 콘텐츠가 업데이트되었음을 ​​메인 스레드에 알리는 방법에 대해 주로 설명합니다. 두 스레드 간에 통신하는 방법은 무엇입니까?

서비스 워커가 메인 스레드와 통신하는 방법

통신이 필요한 이유 우선 디버깅을 하려는 경우 작업자 스레드의 console.log가 DevTools에 표시되지 않습니다. 둘째, 서비스 워커 리소스가 업데이트되면 메인 스레드에 알려야 페이지에서 업데이트 여부를 사용자에게 알리는 메시지를 팝업으로 표시할 수 있습니다. 따라서 의사소통은 비즈니스상 필수일 수 있습니다. Service Worker는 별도의 스레드이므로 기본 스레드와 직접 통신할 수 없습니다. 그러나 일단 통신 문제를 해결하면 여러 가지 멋진 용도로 사용될 수 있습니다. 예를 들어 동일한 사이트의 여러 페이지는 서비스 워커 스레드를 사용하여 페이지 간에 통신할 수 있습니다. 그렇다면 통신 문제를 해결하는 방법은 무엇입니까? 독립적으로 메시지를 보내고 받을 수 있는 두 개의 포트가 있는 메시지 채널 새 MessageChannel()을 만들 수 있습니다. 포트 중 하나인 port2를 서비스 워커에 제공하고 port1 포트는 켜진 상태로 둡니다. 이 채널을 통해 통신할 수 있습니다. 다음 코드는 "작업자 스레드 로그 인쇄", "콘텐츠 업데이트 알림" 및 "애플리케이션 업그레이드"와 같은 기능을 달성하기 위해 두 스레드가 서로 통신하도록 하는 방법을 보여줍니다. 

메인 스레드의 코드 

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;
    }
  }
};

 서비스 워커의 코드

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, 그림 등의 리소스 파일을 압축하면 크기를 크게 줄이고 네트워크 성능을 크게 향상시킬 수 있습니다. 일반적으로 백엔드 서비스는 자동으로 압축 헤더를 구성하지만 더 나은 압축 비율을 얻기 위해 보다 효율적인 압축 알고리즘으로 전환할 수도 있습니다.

콘텐츠 인코딩

웹 사이트를 열고 해당 리소스 네트워크를 살펴보면 응답 헤더에 gzip, 압축, 수축, ID, br 및 기타 값일 수 있는 콘텐츠 인코딩 헤더가 있음을 알 수 있습니다. 비압축을 나타내는 ID 외에도 다른 값을 설정하여 파일을 압축하여 http 전송 속도를 높일 수 있으며, 그 중 가장 일반적인 것은 gzip입니다. 호환성 지원을 통해 br(Brotli)과 같은 일부 최신 압축 형식을 구체적으로 설정하여 gzip을 초과하는 압축률을 달성할 수 있습니다.

글꼴 파일

페이지에 특수 글꼴이 필요하고 페이지의 텍스트가 고정되어 있거나 작은 범위(예: 문자와 숫자만)인 경우 필요한 텍스트만 포함되도록 글꼴 파일을 수동으로 다듬을 수 있습니다. 파일 크기가 대폭 줄어듭니다. .

페이지의 단어가 동적이라면 그 단어가 어떤 단어인지 알 수 없습니다. 사용자가 텍스트를 입력할 때 글꼴 효과를 미리 볼 수 있는 시나리오와 같은 적절한 시나리오에서. 사용자는 일반적으로 몇 단어만 입력하므로 전체 글꼴 패키지를 소개할 필요는 없지만 사용자가 무엇을 입력할지 알 수 없습니다. 따라서 백엔드(또는 nodejs 기반 bff 레이어 구축)가 원하는 단어를 기반으로 몇 개의 단어만 포함된 글꼴 파일을 동적으로 생성하고 이를 반환하도록 할 수 있습니다. 쿼리 요청이 하나 더 있지만 수 Mb, 심지어 10 Mb가 넘는 글꼴 파일의 크기는 몇 kb로 줄어들 수 있습니다.

이미지 형식

사진은 일반적으로 위의 방법을 통해 압축되지 않습니다. 해당 사진 형식은 이미 압축되어 있으며 다시 압축해도 큰 효과가 없기 때문입니다. 따라서 이미지 형식의 선택은 이미지 크기와 이미지 품질에 영향을 미치는 핵심입니다. 일반적으로 압축률이 작을수록 압축 시간이 길어지고 화질이 저하됩니다. 그러나 절대적이지는 않습니다. 새 형식은 이전 형식보다 모든 것이 더 좋을 수 있지만 호환성이 좋지 않습니다. 그래서 균형을 찾아야합니다.

이미지 형식과 관련하여 일반적인 PNG-8/PNG-24, JPEG 및 GIF 외에도 다음과 같은 몇 가지 최신 이미지 형식에 더 많은 관심을 기울이고 있습니다.

  • 웹P
  • JPEG XL
  • AVIF

표를 사용하여 이미지 유형, 투명도 채널, 애니메이션, 인코딩 및 디코딩 성능, 압축 알고리즘, 색상 지원, 메모리 사용량 및 호환성 측면에서 비교합니다.

 

 기술 개발 관점에서는 상대적으로 새로운 이미지 형식(WebP, JPEG XL, AVIF)을 사용하는 것이 우선시됩니다. JPEG XL은 전통적인 이미지 형식을 대체할 것으로 매우 유망하지만 호환성은 여전히 ​​매우 낮습니다. AVIF 호환성은 JPEG XL보다 우수하여 압축 후에도 높은 화질을 유지하고 성가신 압축 아티팩트 및 기타 문제를 방지합니다. 그러나 디코딩 및 인코딩 속도는 JPEG XL만큼 빠르지 않으며 프로그레시브 렌더링은 지원되지 않습니다. WebP는 기본적으로 IE를 제외한 모든 브라우저에서 지원되며 복잡한 이미지(사진 등)의 경우 WebP 무손실 인코딩 성능이 좋지 않지만 손실 인코딩 성능은 매우 좋습니다. 비슷한 품질의 WebP의 이미지 디코딩 속도는 JPEG XL과 크게 다르지 않지만 파일 압축률은 많이 향상될 수 있습니다. 따라서 현재로서는 웹사이트의 이미지 성능을 향상시키려면 기존 형식보다는 WebP를 사용하는 것이 더 나을 것 같습니다.        

그림 요소의 사용

그렇다면 일부 최신 이미지 형식을 지원하는 브라우저에서 위에서 언급한 WebP, AVIF 및 JPEG XL과 유사한 이미지 형식을 자동으로 사용하는 데 도움이 되는 것이 있습니까? 지원하지 않는 브라우저는 일반 JPEG, PNG 방법으로 대체됩니까? HTML5 사양에는 새로운 그림 요소가 추가되었습니다. <picture> 요소는 0개 이상의 <source> 요소와 <img> 요소를 포함하여 다양한 디스플레이/장치 시나리오에 대한 이미지 버전을 제공합니다. 브라우저는 가장 일치하는 하위 <source> 요소를 선택합니다. 일치하는 항목이 없으면 <img> 요소의 src 속성에서 URL을 선택합니다. 선택한 이미지는 <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>

이미지 크기 적응: 물리적 픽셀, 장치 독립적 픽셀

뛰어난 이미지 성능을 원한다면 다양한 크기의 요소에 적절한 이미지 크기를 사용해야 합니다. 500*500 이미지를 100*100 픽셀 영역에 표시한다면 이는 분명 낭비이며, 반대로 500*500 픽셀의 100*100 이미지는 매우 흐릿하여 사용자 경험을 감소시킵니다. 크기 적응에 대해 이야기하기 전에 먼저 장치 독립적 픽셀과 물리적 픽셀이 무엇인지, 그리고 DPR이 무엇인지 이야기해야 합니다.

CSS에 width: 100px이라고 쓰면 화면에 표시되는 것은 실제로 100px 길이의 장치 독립적인 픽셀(논리적 픽셀이라고도 함)이며, 화면의 100픽셀(물리적 픽셀)일 필요는 없습니다. 원래 디스플레이에서는 장치 독립적 픽셀과 물리적 픽셀이 1:1이었습니다. 즉, 너비: 1px는 화면의 1픽셀 발광 지점에 해당합니다. 이후 디스플레이 기술이 발전함에 따라 같은 크기의 화면의 픽셀은 점점 더 정교해졌고, 원래 1픽셀의 위치가 이제는 4픽셀로 구성되었을 수도 있습니다. 이는 더 높은 픽셀 밀도와 더 나은 시각적 경험을 제공하지만 문제도 발생합니다. width: 1px가 이전과 같이 픽셀 광점을 나타내는 경우 픽셀이 이제 더 작아지기 때문에 이 장치에서 동일한 페이지가 축소됩니다. 이 문제를 해결하기 위해 제조업체는 실제 픽셀이 아닌 논리적 픽셀인 장치 독립적 픽셀이라는 개념을 만들었습니다. 이제 장치의 1픽셀이 2개의 더 작은 픽셀로 대체되면 장치의 장치 픽셀 비율(DPR)은 2이고 width: 1px로 그려진 이미지는 2픽셀로 그려지므로 크기와 크기는 이전에는 일관성을 유지하십시오. 마찬가지로, 더 미세한 화면을 가진 장치에서 기존의 1픽셀 크기가 아닌 3개의 더 작은 픽셀로 구성되어 있다고 가정하면 DPR은 3이고 width: 1px는 실제로 3픽셀로 그려집니다. 이제 면접관이 "1px 테두리를 그리는 방법"과 같은 질문을 한 이유를 이해할 수 있습니다. 왜냐하면 높은 DPR에서는 1px가 실제로 1px가 아니기 때문입니다.

따라서 다음과 같은 픽셀 방정식을 얻을 수 있습니다. 1 CSS 픽셀 = 1 장치 독립적 픽셀 = 물리적 픽셀 * DPR.

다양한 DPR 화면에 적합한 그림 제공

따라서 img 요소는 모두 100px이지만 표시해야 하는 최적의 이미지 크기는 실제로 DPR 장치마다 다릅니다. DPR = 2이면 200px 이미지가 표시되어야 하고, DPR = 3이면 300px 이미지가 표시되어야 하며, 그렇지 않으면 흐릿한 조건이 발생합니다.

그렇다면 가능한 해결책은 무엇입니까?

옵션 1: 간단하고 대략적인 다중 그래프

이제 일반 장치에서 가장 높은 DPR은 3이므로 가장 간단한 방법은 기본적으로 가장 높은 3x 이미지 디스플레이를 사용하는 것입니다. 그러나 이로 인해 대역폭이 많이 낭비되고 네트워크 성능이 저하되며 사용자 경험이 저하됩니다. 이는 확실히 우리 기사의 "스타일"과 일치하지 않습니다.

옵션 2: 미디어 문의

@media 미디어 쿼리를 사용하여 현재 장치의 DPR을 기반으로 다양한 CSS를 적용할 수 있습니다.

#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]);
  }
}

 이 솔루션의 장점은 다양한 DPR에서 다양한 배율로 사진을 표시할 수 있다는 것입니다.

이 솔루션의 단점은 다음과 같습니다.

  • 많은 논리적 분기가 있으며 시중에는 DPR = 2 또는 3인 장치뿐만 아니라 십진수 DPR을 갖는 일부 장치도 있으므로 모든 것을 다루려면 많은 코드를 작성해야 합니다.
  • 구문 호환성 문제. 예를 들어 일부 브라우저에서는 -webkit-min-device-pixel-ratio입니다. autoprefixer를 사용하여 이 문제를 해결할 수 있지만 이로 인해 추가 비용이 발생합니다.

 옵션 3: CSS 이미지 세트 구문

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

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

 그 중 2x와 3x는 서로 다른 DPR과 일치합니다. 이미지 세트 솔루션의 단점은 미디어 쿼리와 동일하므로 자세히 설명하지 않겠습니다. 장점은 미디어 쿼리보다 더 틈새 시장에 속하며 물결인 것처럼 가장할 수 있다는 것입니다.

옵션 4: srcset 요소 속성

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

내부의 2x 및 3x는 서로 다른 DPR이 일치함을 나타내며 [email protected]가 최종선입니다. 장점과 단점은 이미지셋과 동일하며, CSS를 작성할 필요가 없고 더 간결하다는 장점이 있을 수 있습니다.

옵션 5: srcset 속성과 크기 속성 결합 

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

size="(min-width: 600px) 600px, 300px"는 화면의 현재 CSS 픽셀 너비가 600px보다 크거나 같으면 이미지의 CSS 너비가 600px임을 의미합니다. 그렇지 않은 경우 이미지의 CSS 너비는 300px입니다. 레이아웃이 유연할 수 있으므로 img 요소의 크기는 화면 크기에 따라 다를 수 있습니다. 위의 다른 솔루션은 DPR을 기준으로만 판단할 수 있으며 이는 달성할 수 없습니다. 크기는 또한 너비 임계값을 기반으로 img의 너비를 실제로 변경하려면 @media가 필요합니다.

srcset="[email protected] 300w, [email protected] 600w, [email protected] 900w" 안에 있는 300w, 600w, 900w를 너비 설명자라고 합니다. DPR이 2이고 img 요소의 CSS 픽셀이 크기를 기준으로 300인 장치를 사용하는 경우 실제 물리적 픽셀은 600이므로 600w 이미지가 사용됩니다.

이 솔루션의 단점은 여전히 ​​이전과 동일하며, 서로 다른 DPR에 대해 서로 다른 그림을 작성해야 합니다. 하지만 반응형 레이아웃의 img 요소 크기에 따라 실제 이미지 해상도를 유연하게 변경할 수 있다는 고유한 장점이 있습니다. 그러므로 나는 옵션 5를 추천한다.

이미지의 지연 로딩 및 비동기 디코딩

이미지의 지연 로딩은 페이지가 대상 영역으로 스크롤되지 않았을 때 해당 이미지가 요청 및 표시되지 않아 가시 영역의 콘텐츠 표시 속도를 높이는 것을 의미합니다. 현재 프런트 엔드 사양은 매우 풍부하며 js, html 및 이미지 지연 로딩을 구현하는 기타 방법이 있습니다. 

옵션 1: js에서 onscroll 사용

이것은 간단하고 조잡한 해결책입니다. getBoundingClientRectAPI를 통해 뷰포트 상단에서 페이지에 있는 모든 그림의 거리를 가져오고, onscroll 이벤트를 통해 페이지 스크롤을 모니터링한 다음, 뷰포트 높이를 기준으로 가시 영역에 어떤 그림이 나타나는지 계산합니다. , img 요소의 src 속성을 설정합니다. 이미지 로딩을 제어하는 ​​값입니다.

이 솔루션의 장점은 로직이 간단하고 이해하기 쉽고, 새로운 API가 사용되지 않으며, 호환성이 좋다는 것입니다.

이 솔루션의 단점은 다음과 같습니다.

  1. 코드량과 계산 비용이 발생하는 js 도입 필요
  2. 추가 리플로우를 유발할 수 있는 모든 이미지 요소의 위치 정보를 얻어야 합니다.
  3. 항상 스크롤을 모니터링하고 콜백을 자주 트리거해야 함
  4. 스크롤 목록이 페이지에 중첩된 경우 이 솔루션은 중첩 스크롤 목록에 있는 요소의 가시성을 알 수 없으며 더 복잡한 쓰기가 필요합니다.

옵션 2: js에서 IntersectionObserver 사용

HTML5의 IntersectionObserver API를 통해 Intersection Observer(교차 관찰자)는 모니터링 요소의 isIntersecting 속성과 협력하여 요소가 가시 영역 내에 있는지 확인하고 스크롤 모니터링보다 더 나은 성능으로 이미지에 대한 지연 로딩 솔루션을 구현할 수 있습니다. 관찰된 요소는 가시 영역에 나타나거나 사라질 때 콜백을 트리거하며, 출현 비율의 임계값도 제어할 수 있습니다. 자세한 내용은 mdn 설명서를 참조하세요.

이 솔루션의 장점은 다음과 같습니다.

  1. 성능은 onscroll보다 훨씬 낫습니다. 항상 스크롤을 모니터링할 필요도 없고 요소 위치를 얻을 필요도 없습니다. 가시성은 그릴 때 렌더 스레드에서 알 수 있으며 js를 통해 판단할 필요가 없습니다. .. 이렇게 쓰는 방식이 더 자연스럽습니다.
  2. 요소의 가시성을 실제로 알 수 있습니다.예를 들어 요소가 상위 요소에 의해 차단되면 이미 가시 영역에 표시되어 있어도 표시되지 않습니다. 이는 onscroll 솔루션이 수행할 수 없는 작업입니다.

이 솔루션의 단점은 다음과 같습니다.

  1. 코드량과 계산 비용이 발생하는 js 도입 필요
  2. 이전 장치는 호환되지 않으며 폴리필을 사용해야 합니다.

옵션 3: CSS 스타일 콘텐츠 가시성

content-visibility: auto 스타일을 가진 요소가 현재 화면에 없으면 해당 요소는 렌더링되지 않습니다. 이 방법을 사용하면 보이지 않는 영역의 요소 그리기 및 렌더링 작업을 줄일 수 있지만 HTML을 구문 분석할 때 이미지 리소스가 요청되므로 이 CSS 솔루션은 이미지의 지연 로딩을 실제로 구현할 수 없습니다.

해결 방법 4: HTML 속성 로딩=게으름

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

그림 비동기 디코딩 솔루션

우리 모두 알고 있듯이 jpeg, png 등의 이미지는 인코딩되는데, GPU가 이를 인식하고 렌더링하도록 하려면 디코딩이 필요합니다. 특정 이미지 형식이 매우 느리게 디코딩되면 다른 콘텐츠의 렌더링에 영향을 미칩니다. 따라서 HTML5에는 이미지 데이터를 구문 분석하는 방법을 브라우저에 알려주는 새로운 디코딩 속성이 추가되었습니다.

선택적 값은 다음과 같습니다.

  • sync: 이미지를 동기적으로 디코딩하여 다른 콘텐츠와 함께 표시되도록 합니다.
  • async: 이미지를 비동기적으로 디코딩하여 다른 콘텐츠 표시 속도를 높입니다.
  • auto: 기본 모드로, 디코딩 모드가 선호되지 않음을 나타냅니다. 사용자에게 어떤 방법이 더 적합한지 결정하는 것은 브라우저에 달려 있습니다.
<img src="xxx.jpeg" decoding="async" />

이를 통해 브라우저는 이미지를 비동기적으로 디코딩하여 다른 콘텐츠 표시 속도를 높일 수 있습니다. 이는 이미지 최적화 계획의 선택적인 부분입니다.

이미지 성능 최적화 요약

일반적으로 이미지 성능 최적화를 위해서는 다음이 필요합니다.

  1. 압축률이 높고 디코딩 속도가 빠르며 이미지 품질이 좋은 이미지 형식을 선택하세요.
  2. 실제 DPR 및 요소 크기에 따라 적절한 이미지 해상도를 조정합니다.
  3. 이미지의 지연 로딩을 위해 더 나은 성능 솔루션을 사용하고 상황에 따라 비동기 디코딩을 사용하십시오.

빌드 도구 최적화

현재 인기 있는 프런트 엔드 구축/패키징 도구가 많이 있습니다. 예를 들어 최근 몇 년 동안 인기를 끌었던 오래된 도구 webpack, , , 새로운 세력 , 등이 있습니다. 그 중 일부는 js로 구현되고, 일부는 go, Rust 등 고성능 언어로 작성되며, 일부 구성 도구는 온디맨드 패키징을 위해 esm 기능을 사용합니다. 하지만 이는 개발이나 구축 중 속도 최적화에 관한 것이며 클라이언트 성능과는 거의 관련이 없으므로 여기서는 다루지 않고 주로 프로덕션 환경 패키징을 통한 네트워크 성능 최적화에 대해 이야기하겠습니다. 이러한 도구들은 다양한 구성을 가지고 있지만 일반적으로 사용되는 최적화 포인트는 코드 압축, 코드 분할, 공개 코드 추출, CSS 추출, 리소스 사용 CDN 등이 있는데 구성 방법이 다르므로 이에 대한 문서를 확인하면 됩니다. 많은 것은 즉시 사용 가능합니다. 어떤 분들은 이 단어를 잘 이해하지 못하실 수도 있는데, 이에 대한 설명은 다음과 같습니다.rollupvitesnowpackesbuildswcturbopack

코드 압축에 대해서는 따로 설명할 것이 없고, 코드를 작게 만들기 위해 변수 이름 바꾸기, 개행 제거, 공백 제거 등을 의미합니다.

코드 분할의 목적은 예를 들어 SPA에서 페이지 A가 로컬 라우팅을 통해 홈페이지에서 리디렉션되므로 사용자가 반드시 점프할 필요가 없기 때문에 홈페이지의 기본 애플리케이션과 함께 A 페이지 구성 요소를 패키징할 필요가 없습니다. 동시에 홈페이지 패키지의 크기가 커지고 첫 화면의 속도에 영향을 줍니다. 따라서 일부 빌드 도구에서는 동적 가져오기( )를 사용할 수 import('PageA.js')있으며 빌드 도구는 홈페이지에서 참조된 페이지 A 코드를 새 패키지로 패키징합니다(예: ) a.js. 사용자가 페이지 A로 이동하기 위해 홈페이지를 클릭하면 a.js내부의 구성 요소 코드가 자동으로 요청되고 경로가 전환되어 렌더링됩니다. 일부 프레임워크는 즉시 작동하며 동적 가져오기를 작성할 필요가 없습니다. 경로를 정의하기만 하면 자동으로 코드가 분리됩니다(예: React의 nextjs 프레임워크). 이것은 코드 분리의 하나의 사용 시나리오일 뿐입니다. 간단히 말해서, 특정 모듈 코드가 기본 애플리케이션과 함께 패키지되는 것을 원하지 않는 한 이를 분할하여 첫 번째 js 패키지 배치의 더 나은 성능을 얻을 수 있습니다.

공통 코드 추출의 목적은 SPA를 작성한다고 가정할 때 A, B, C 페이지에서 ramda 라이브러리를 사용하고 코드가 이 세 페이지로 분할되어 이제 세 개의 독립적인 패키지인 , a.js, b.js입니다 c.js. 따라서 일반적인 논리에 따르면 ramda 라이브러리는 해당 종속성으로 이 세 패키지에도 포함됩니다. 이는 이 세 페이지에 아무 이유 없이 중복된 ramda 코드가 있음을 의미합니다. 그러면 안 좋은데 가장 좋은 방법은 ramda 라이브러리를 메인 애플리케이션에 별도의 패키지로 넣어서 한 번만 요청하면 ABC가 이 라이브러리를 사용할 수 있도록 하는 것입니다. 이것이 일반적인 코드 추출이 수행하는 작업입니다. 예를 들어 webpack에서는 모듈이 공통 청크로 별도의 패키지로 추출되기 전에 모듈이 반복적으로 의존하는 횟수를 정의할 수 있습니다.

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

 하지만 webpack4부터는 모드를 통해 자동으로 최적화를 도와줄 수 있기 때문에 실제로는 이런 걱정을 할 필요가 없습니다. 불필요한 최적화를 피하기 위해 사용하는 빌드 도구의 문서에 대해 자세히 알아볼 수 있습니다.

CSS 추출의 목적은 예를 들어 webpack에서 css-loader + style-loader만 사용하면 CSS가 js로 컴파일되고, js는 스타일을 렌더링할 때 스타일을 삽입하는 데 도움이 됩니다. 그러면 js가 눈에 보이지 않게 커지고 CSS 스타일 렌더링은 js가 실행될 때까지 지연되며 js는 일반적으로 페이지 끝에 패키지됩니다. 즉, 최종 js 요청 및 실행이 완료될 때까지 페이지는 남아 스타일이 없습니다. 이상적인 상황은 css와 dom이 병렬로 파싱되고 렌더링되므로 css를 추출해야 하며, css를 별도로 css 파일로 패키징하여 html에 넣지 않고 html 시작 부분의 링크 태그에 넣습니다. js.js.

트리 쉐이킹 최적화

우리는 패키징 도구가 패키징 시 esm의 Tree Shaking을 기반으로 데드 코드를 제거하는 데 도움이 된다는 것을 알고 있습니다.

예를 들어, bar.js가 있습니다.

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

export const fn2 = () => {};

 그런 다음 index.js에서 fn1 함수를 사용하세요.

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

fn1();

 index.js를 패키징의 진입점으로 사용하면 결국 fn2가 제거됩니다.

그러나 일부 시나리오에서는 트리 쉐이킹이 실패합니다. 코드에 "부작용"이 없어야 합니다. 즉, 함수형 프로그래밍의 부작용과 유사하게 초기화 중에 외부 세계에 영향을 줄 수 없어야 합니다.

다음 예를 살펴보십시오.

// 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();

fn3 및 fn4는 사용되지 않지만 최종 패키지에는 포함됩니다. 선언하면 인쇄, 외부 변수 수정 등의 부작용이 있기 때문입니다. 이를 유지하지 않으면 기대와 다른 버그가 발생할 수 있습니다. 예를 들어 창이 변경되었다고 생각하지만 실제로는 변경되지 않았습니다. 객체 속성에는 setter가 있을 수 있으며, 심지어 예상치 못한 버그가 더 있을 수도 있습니다.

또한 다음과 같은 작성 방법도 허용되지 않습니다.

// 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()

내보낸 것을 객체에 넣을 수 없습니다. esm의 Tree Shaking은 정적 분석이므로 런타임에 수행되는 작업을 알 수 없습니다. Commonjs 모듈식 구문도 사용됩니다.패키징 도구는 혼합 사용과 호환될 수 있지만 Tree Shaking이 쉽게 실패할 수 있습니다.

따라서 Tree Shaking 기능을 충분히 활용하기 위해서는 글쓰기 방법에 주의를 기울여야 합니다. 온라인에 접속하기 전에 포장 분석 도구를 사용하여 크기가 비정상적인 패키지를 확인할 수 있습니다.

프런트엔드 기술 스택 최적화

기술 스택의 선택은 런타임 속도에 영향을 미칠 뿐만 아니라 네트워크 속도에도 영향을 미칠 수 있습니다.

더 작은 라이브러리로 교체되었습니다. 예를 들어 lodash를 사용한다면 그 안에 하나의 함수만 사용해도 commonjs 기반이고 Tree Shaking을 전혀 하지 않기 때문에 그 안의 모든 내용을 패키징하게 됩니다. , 다른 것을 사용하는 것을 고려해 볼 수 있습니다.라이브러리 교체.

개발 방식으로 인한 코드 중복. 예를 들어, sass, less, 기본 CSS, styled-Component, Emotion 등과 같은 스타일 솔루션을 사용하는 경우 반복되는 스타일 코드를 작성하는 것은 쉽습니다. 예를 들어 구성 요소 A와 구성 요소 B는 모두 너비가 120px입니다. 두 번 작성할 가능성이 높습니다. 세분화된 재사용을 달성하기가 어렵습니다. (거의 아무도 그렇게 하지 않을 것입니다.) 한 줄의 스타일을 반복하세요.) (재사용을 생각하기 전에는 7~8줄이 동일할 수도 있습니다.) 프로젝트가 클수록, 오래될수록 스타일 코드가 더 많이 반복되고, 그러면 리소스 파일도 점점 더 커질 것입니다. . 원자 CSS 라이브러리인 tailwindcss로 변경할 수 있습니다. 너비: 120px; 스타일이 필요한 경우 이에 대응하여 <div className="w-[120px]"></div>를 작성할 수 있습니다. 공식은 다음과 같습니다. 모두 이렇게 작성되었으며 모두 동일한 클래스를 재사용합니다. tailwind를 사용하면 런타임 오버헤드 없이 CSS 리소스를 충분히 작게 유지할 수 있습니다. 동시에 컴포넌트를 따르기 때문에 esm의 트리 쉐이킹을 활용할 수 있으며, 더 이상 사용되지 않는 일부 컴포넌트는 해당 스타일과 함께 자동으로 패키지에서 제거됩니다. sass, css 및 기타 솔루션의 경우 CSS 파일에서 더 이상 사용되지 않는 스타일을 자동으로 제거하기가 어렵습니다. 또한 Styled-Component 및 Emotion과 같은 CSS-in-JS 솔루션도 트리 쉐이킹을 달성할 수 있지만 코드 중복 및 런타임 오버헤드 문제가 있습니다. 하위 버전의 nodejs를 지원하지 않는 등 tailwind의 몇 가지 단점도 있으며, 문법에는 학습 비용이 있습니다.

런타임 레벨

런타임은 주로 기술 스택 최적화, 멀티 스레드 최적화, V8 수준 최적화, 브라우저 렌더링 최적화 등을 포함하는 JavaScript 및 페이지 렌더링을 실행하는 프로세스를 나타냅니다. 

렌더링 시간을 최적화하는 방법

렌더링 시간은 DOM 및 스타일의 복잡성뿐만 아니라 여러 측면의 영향을 받습니다.

렌더링 스레드에는 다양한 종류의 작업이 있습니다.

이 섹션에 대해 이야기하기 전에 먼저 작업의 개념에 대해 이야기해야 합니다. 일부 사람들은 스크립트의 코드 및 일부 콜백(이벤트, setTimeout, ajax 등)과 같은 매크로 작업에 대해 이미 어느 정도 이해하고 있을 수도 있습니다. 그러나 매크로 작업의 세부 사항만 이해하고 작업에 대한 더 넓은 이해가 부족할 수 있습니다. 더 높은 수준에서 이해해야 js와 렌더링을 차단해야 하는 이유와 두 매크로 사이에 간격이 없는 이유를 진정으로 이해할 수 있습니다. 서로 옆에 있는 작업으로 신속하게 구현되지 않을 수 있습니다.

페이지를 열면 브라우저는 렌더링 스레드가 포함된 렌더링 프로세스를 시작합니다. dom, css 렌더링, js 실행과 같은 대부분의 프런트엔드 작업은 이 렌더링 스레드에서 실행됩니다. 단일 스레드만 있기 때문에 시간이 많이 걸리는 작업을 차단하지 않고 처리하기 위해 작업 대기열을 설계하고 요청, IO 등의 작업이 발생하면 다른 스레드로 넘겨주고 완료 후에는 콜백을 수행합니다. 큐와 렌더링 스레드에 배치됩니다. 이 큐의 헤드 작업은 항상 폴링되고 실행됩니다. js의 대부분 작업은 매크로 작업으로 이해될 수 있습니다. 그런데 js만이 아니고 페이지 렌더링도 렌더링 스레드를 위한 작업인데 DevTools에서 성능상 렌더링을 담당하는 작업을 볼 수 있습니다.(Parse HTML, 레이아웃, 페인트 등 일련의 작업으로 구성되어 있습니다.) 등), js 소위 매크로 작업의 실행은 실제로 평가 스크립트 작업(컴파일 코드, 캐시 스크립트 코드 및 런타임 컴파일, 캐싱 코드 등을 담당하는 기타 하위 작업 포함)입니다. Parse HTML 작업의 하위 작업이 되어야 합니다. GC 가비지 수집과 같은 기본 제공 작업도 많이 있습니다. 성능상 마이크로태스크 실행을 의미하는 마이크로태스크라는 특별한 종류의 작업도 있는데, 이는 매크로 작업에서 생성되어 매크로 작업 내의 마이크로 작업 대기열에 배치됩니다. 매크로태스크가 실행되고 모든 실행 스택이 종료되면 체크포인트가 생기고, 마이크로태스크 대기열에 마이크로태스크가 있으면 모두 실행됩니다. Promise.then, queueMicrotask, MutationObserver 이벤트, 노드의 nextTick 등과 같은 마이크로태스크를 생성할 수 있습니다.

따라서 이제 렌더링 스레드의 작업을 이해했으므로 렌더링 자체도 작업이므로 js 작업 및 다른 작업과 함께 대기열에서 순차적이어야 하며 하나씩 실행되어야 한다는 것을 어렵지 않게 알 수 있습니다. .이렇게 차단이 발생합니다. 다양한 리소스 간의 차단 관계를 살펴보겠습니다.

js 차단 렌더링의 일반적인 예를 제공하려면 직접 html 파일을 만들어 시도해 볼 수 있습니다.

<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>

 렌더링 스레드는 먼저 Parse HTML 작업을 실행하고, DOM의 파싱 과정에서 스크립트를 만나면 Evaluate Script가 실행되며, 코드는 종료되기 전 3초 동안 실행된 후 계속해서 다음 <을 파싱하고 렌더링하게 됩니다. div>페이지</div>이므로 페이지가 나타나는 데 3초 정도 걸립니다. 스크립트가 원격 리소스인 경우 요청은 기본 DOM 구문 분석 및 렌더링도 차단합니다.

스크립트의 defer 속성을 통해 이를 최적화할 수 있습니다. Defer는 DOM이 구문 분석된 후 DOMContentLoaded 이벤트가 발생하기 전까지 스크립트의 실행 시간을 지연시킵니다.

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

 이렇게 하면 요청을 기다리는 데 시간을 낭비할 필요가 없으며 동시에 요소를 보다 안전하게 얻기 위해 DOM에서 js를 구문 분석해야 하며 여러 연기 스크립트가 원래 실행 순서를 보장합니다. 또는 페이지 하단에 직접 스크립트를 작성하여 유사한 효과를 얻을 수 있습니다. 하단에 적힌 스크립트 요청이 지연될까 걱정하지 마세요. 브라우저에는 일반적으로 HTML의 모든 리소스에 대한 요청을 미리 스캔하여 문서 파싱이 시작될 때 미리 요청하는 최적화 메커니즘이 있습니다.

스크립트에는 또 다른 속성인 async가 있습니다.js 리소스가 여전히 요청 중인 경우 js 요청 및 실행도 건너뜁니다. js 요청이 완료된 후 다음 내용을 먼저 구문 분석하고 즉시 실행합니다. 따라서 실행 시점은 고정되어 있지 않고 요청 종료 시점에 따라 달라지며, 여러 비동기 스크립트의 실행 순서는 보장되지 않습니다.

CSS가 렌더링과 JS를 차단합니까?

여기서 결론 한가지만 기억하세요, CSS의 요청과 파싱은 아래 DOM의 파싱을 차단하지는 않지만, 렌더 트리 렌더링과 js의 실행을 차단할 것입니다.

왜 이렇게 설계되었는지는 다음과 같습니다.

렌더트리는 원래 DOM트리에 적용된 Cascading Style Sheet의 산물이기 때문에 차단되어 있으므로 CSS를 기다려야 합니다.Css를 기다리지 않도록 설계되어 있지만 문제가 없습니다.dom트리를 렌더링하면 됩니다. 먼저 전체 렌더 트리를 렌더링한 다음 두 번 렌더링하는 것은 낭비이고 베어 DOM 트리의 사용자 경험은 좋지 않습니다.

js가 css에 의해 차단되는 이유는 js에서 스타일을 수정할 수 있기 때문일 수 있습니다. 이후 js를 실행하고 스타일을 먼저 수정한 후 이전 css를 적용하면 스타일이 일치하지 않는 결과가 발생합니다. 실제 예상되는 효과를 얻기 위해 js에서 스타일을 두 번 렌더링하는 것은 낭비입니다. 그리고 요소 스타일은 js에서 얻을 수 있는데, CSS 요청이 파싱되기 전에 다음 js를 실행하면 얻은 스타일이 실제 상황과 일치하지 않게 됩니다.

요약하면 CSS는 dom의 구문 분석을 직접 차단하지는 않지만 렌더 트리의 렌더링을 차단하고 js의 실행을 차단하여 dom의 구문 분석을 간접적으로 차단합니다.

관심이 있으시면 직접 노드 서비스를 구축하여 실험해 보실 수도 있고, 리소스 응답 시간을 제어하여 다양한 리소스의 상호 영향을 테스트할 수도 있습니다.

브라우저 렌더링에 시간이 오래 걸리는 이유는 무엇입니까? 렌더링 파이프라인이란 무엇입니까?

HTML을 페이지로 렌더링하려면 일반적으로 다음 단계가 필요합니다.

  • DOM 트리 생성: HTML을 가져올 때 페이지가 나타나도록 브라우저가 정확히 무엇을 했나요? 먼저 내부의 모든 리소스 요청을 사전 구문 분석하고 사전 요청을 발행합니다. 그런 다음 html을 어휘적, 문법적으로 파싱하고 <body>, <div>와 같은 요소 태그와 class, id와 같은 속성을 만나면 파싱하여 dom 트리를 생성합니다. 이 기간 동안 <style>, <link>, <script>와 같은 css 및 js 태그가 나타날 수 있습니다. css 리소스 요청은 dom 트리 구문 분석을 차단하지 않습니다. dom 트리가 css 구문 분석을 마친 경우 그때까지 파싱되지 않으므로 렌더 트리 및 후속 레이아웃 트리 생성 등을 차단합니다. 코드 실행이든 리소스 요청이든 js를 발견하면 스크립트 태그에 async 또는 defer 속성이 없는 한 DOM 구문 분석을 계속하기 전에 모든 실행이 완료될 때까지 기다립니다. js 앞에 CSS 리소스가 있는 경우 CSS가 요청/파싱될 때까지 js가 실행되지 않으므로 CSS가 간접적으로 DOM 구문 분석을 차단하게 됩니다. CSS 관련 코드가 발견되면 CSS를 스타일시트로 구문 분석하는 다음 단계가 수행됩니다.
  • 스타일시트 생성: CSS도 어휘 및 문법 분석을 거쳐 일부 값이 표준화됩니다. 표준화란 무엇입니까? 예를 들어 작성한 Font-weight:bold 및 flex-flow:column nowrap은 실제로 표준 CSS 스타일이 아니라 약어입니다. 엔진이 이해할 수 있는 값(font-weight)으로 변환해야 합니다. : 500 , flex-direction: 열, flex-wrap: nowrap. 마지막으로 직렬화된 텍스트는 구조화된 스타일시트가 됩니다.
  • 렌더 트리 생성: DOM 트리 및 스타일시트를 사용하면 상속, CSS 선택기 우선순위 및 기타 스타일 규칙을 통해 해당 DOM에 스타일을 추가할 수 있습니다. CSS 선택기는 오른쪽에서 왼쪽으로 조건을 일치시키므로 일치하는 항목 수가 상대적으로 적습니다. 최소한으로, 결국 스타일을 사용하여 렌더 트리를 형성합니다.
  • 레이아웃: display: none과 같은 일부 DOM은 표시되지 않으므로 렌더링 트리를 기반으로 레이아웃 트리가 형성되며, 여기에는 잘못된 계산을 피하기 위해 향후 나타날 노드만 포함됩니다. 동시에 레이아웃 단계에서는 각 요소의 레이아웃 위치 정보를 계산하는데 이는 시간이 많이 걸리고 요소 위치가 서로 영향을 미칩니다.
  • 레이어: 그런 다음 위치: 절대, 변환, 불투명도 등과 같은 일부 특수 스타일에 따라 다양한 레이어가 형성됩니다. 루트 노드와 스크롤도 하나의 레이어로 계산됩니다. 서로 다른 레이어 레이아웃은 일반적으로 서로 영향을 미치지 않기 때문에 레이어링을 사용하면 후속 업데이트에서 레이아웃 비용을 줄일 수 있으며 후속 합성 레이어를 통해 개별 레이어에 특수 변환을 수행할 수도 있습니다.
  • 페인트(그리기): 실제로 디스플레이에 그리는 것이 아니라, 레이어별로 자체적인 그리기 명령을 생성하는 명령으로 직선 그리기 등 GPU 그리기의 기본 명령입니다.
  • Composite(컴포지트): 이 단계에서 CPU는 더 이상 작업을 실행하지 않고 작업은 처리를 위해 GPU로 넘겨지므로 js가 차단되어도 이 스레드에는 영향을 미치지 않습니다. CSS 하드웨어 가속도 다음에서 발생합니다. 이 스레드. 페인트 단계의 그리기 명령 목록은 컴포지션 레이어로 전달되며 컴포지션 레이어는 현재 뷰포트 근처 영역을 512px 단위로 타일로 나누고 타일 영역을 먼저 렌더링합니다. 뷰포트는 자유로워질 때까지 기다릴 수 있습니다. 다시 렌더링하세요. 컴포지션 레이어는 래스터화 스레드 풀을 통해 그리기 명령을 GPU에 전달하여 비트맵을 그려 출력하는데, 이러한 비트맵은 각 레이어에 속하므로 컴포지션 레이어에서 이러한 레이어를 하나의 비트맵으로 합성해야 합니다. 레이어가 래스터화되면 합성 레이어는 여러 레이어를 합성하여 올바른 순서로 쌓아 최종 렌더링을 형성할 수 있습니다. 이 프로세스는 일반적으로 CPU의 작업량을 줄이고 렌더링 성능을 향상시키기 위해 GPU에서 수행됩니다.

래스터화 설명: 래스터화는 컴퓨터 그래픽의 개념입니다. 컴포지션 레이어에서 레이어의 벡터 그래픽, 텍스트, 그림 및 기타 요소를 비트맵 또는 래스터 이미지로 변환합니다. 이렇게 하면 비트맵이 그래픽 하드웨어에서 더 효율적으로 처리되기 때문에 이러한 요소를 더 빠르게 렌더링하고 표시할 수 있습니다. 컴포지션 레이어는 렌더링해야 하는 콘텐츠를 화면에 직접 렌더링하는 대신 오프스크린 메모리 영역에 그릴 수 있습니다. 이를 통해 화면에 직접 그릴 때 발생하는 성능 문제를 방지하고 브라우저가 백그라운드에서 화면 밖의 콘텐츠를 최적화할 수 있습니다. 레이어 콘텐츠를 래스터화함으로써 브라우저는 렌더링을 위해 그래픽 하드웨어 가속을 더 잘 활용할 수 있습니다. 최신 컴퓨터 및 모바일 장치의 그래픽 처리 장치(GPU)는 비트맵 이미지를 효율적으로 처리하여 더 부드러운 애니메이션과 더 빠른 렌더링 속도를 제공합니다.

  • 디스플레이: 모니터가 동기화 신호를 보낼 때까지 기다립니다. 이는 다음 프레임이 곧 표시된다는 의미입니다. 복합 레이어의 비트맵은 브라우저 프로세스에서 biz 구성 요소로 전달되고 비트맵은 백 버퍼에 저장되며, 모니터에 다음 프레임이 표시되면 프런트 버퍼와 백 버퍼가 교환되어 최신 프레임이 표시됩니다. 페이지 이미지. js의 requestAnimationFrame 콜백도 동기화 신호가 다음 프레임이 곧 렌더링될 것임을 알고 있기 때문에 트리거됩니다. 게임에는 수직 동기화도 있습니다.

렌더링 중에는 이 단계들이 파이프라인처럼 순차적으로 실행되는데, 파이프라인이 특정 단계부터 실행을 시작하면 필연적으로 끝까지 다음 단계를 모두 실행하게 된다.

따라서 위치/레이아웃 관련 스타일이 수정되어 페이지가 업데이트되면 2단계 레이아웃이 다시 트리거되어 레이아웃을 다시 계산하는 것을 리플로우(reflow 또는 reflow)라고 합니다. 수많은 요소의 위치를 ​​계산해야 하고, 그 위치가 서로 영향을 미치기 때문에 이 단계가 매우 시간이 많이 걸리는 것을 알 수 있습니다. 동시에 페인트, 합성 등 모든 후속 단계도 실행되므로 리플로우에는 리페인트가 수반되어야 합니다.

위치 독립적 스타일 수정(예: 배경색, 색상)으로 인해 페이지가 업데이트되는 경우 이전 프로세스가 의존하는 데이터가 변경되지 않았기 때문에 4단계 페인트에서만 다시 트리거됩니다. 이렇게 하면 그리기 명령이 재생성된 다음 합성 레이어에서 래스터화 및 합성됩니다. 전체 프로세스는 여전히 매우 빠르므로 다시 그리는 것이 재정렬하는 것보다 훨씬 빠릅니다.

성능을 향상시키기 위해 렌더링 원리를 사용하는 방법

브라우저 자체에는 몇 가지 최적화 방법이 있는데, 예를 들어 색상: 빨간색, 너비: 120px의 경우 순서 문제로 인해 반복 페인트가 발생하는 것을 걱정할 필요가 없으며, 여러 연속으로 인한 성능 저하를 걱정할 필요가 없습니다. 스타일 수정 및 여러 연속 추가 요소. . 브라우저는 수정 후 즉시 렌더링을 시작하지 않고 업데이트를 대기 대기열에 넣은 다음 특정 수정 횟수 또는 일정 기간 후에 일괄적으로 업데이트합니다.

페이지를 업데이트하는 코드를 작성할 때 원칙은 가능한 한 적은 수의 렌더링 파이프라인을 트리거하는 것입니다.페인트 단계에서 시작하는 것이 레이아웃 단계보다 훨씬 빠릅니다. 다음은 몇 가지 일반적인 고려 사항입니다.

  1. 간접 리플로우를 피하세요. 위치 관련 스타일을 직접 수정하는 것 외에도 상황에 따라 레이아웃을 간접적으로 수정할 수도 있습니다. 예를 들어, box-sizing이 border-box가 아니고 너비가 고정되어 있지 않은 경우 border-width를 추가하거나 수정하면 상자 모델의 너비와 레이아웃 위치에 영향을 미칩니다. 예를 들어 <img />는 높이를 지정하지 않으므로 로드 후 이미지 높이가 올라가 페이지가 리플로우됩니다.
  2. 읽기와 쓰기의 분리 원리. Node.js가 요소 위치 정보를 얻으면 getBoundingClientRect, offsetTop 등과 같은 강제 리플로우가 트리거될 수 있습니다. 앞서 말씀드린 바와 같이 브라우저는 일괄적으로 업데이트되며 대기줄이 있기 때문에 위치정보를 얻을 때 업데이트로 인해 대기줄이 지워지지 않을 수도 있고, 페이지가 최신이 아닐 수도 있습니다. 얻은 데이터가 정확한지 확인하기 위해 브라우저는 대기열을 강제로 지우고 페이지를 강제로 리플로우합니다. 두 번째로 위치 정보를 획득하고 해당 기간 동안 업데이트가 발생하지 않으면 대기 대기열이 비어 있고 리플로우가 다시 트리거되지 않습니다. 따라서 요소 배치의 크기를 일괄 수정하고 해당 크기 정보를 얻으려면 다음과 같이 작성하면 안 됩니다.
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);
}

 브라우저 일괄 업데이트 및 강제 리플로우에 대한 위의 설명을 보면 이런 방식으로 작성하는 것이 매우 문제가 많으며 페이지가 1000번 리플로우된다는 것을 알 수 있습니다! style.width를 수정할 때마다 브라우저는 업데이트를 대기 대기열에 넣기 때문입니다. 이 단계에는 아무런 문제가 없습니다. 그러나 이 요소의 너비를 얻기 시작하면 최신 너비를 알기 위해 브라우저는 대기 대기열을 지우고 일괄 업데이트를 건너뛰고 페이지를 강제로 리플로우합니다. 그런 다음 이 작업을 1000번 동안 계속하세요.

그리고 이렇게 작성하면 1000개의 크기 수정이 한 번만 리플로우됩니다.

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에 의해서만 계산된다면 속도는 필연적으로 매우 빨라질 것입니다. 이것이 바로 하드웨어 가속입니다. 어떤 방법을 켤 수 있나요?

3D 변환 및 불투명도와 같은 CSS 스타일에는 리플로우 및 다시 그리기가 포함되지 않고 레이어 변환만 포함됩니다. 따라서 이전 레이아웃 및 페인트 단계를 건너뛰고 컴포지션 레이어로 직접 전달됩니다. GPU는 레이어에서 몇 가지 간단한 변환을 수행합니다. GPU가 이러한 일을 처리하는 것은 매우 간단합니다. 하드웨어 가속에 특별히 사용되는 will-change라는 CSS 속성도 있는데, GPU에 앞으로 어떤 속성이 바뀔지 미리 알려주므로 미리 대비할 수 있습니다.

한 가지 주목할 점은 js를 사용하여 스타일을 수정할 때 하드웨어 가속이 가능한 위 스타일을 수정하더라도 여전히 CPU를 통과한다는 것입니다. 렌더링 파이프라인을 기억하시나요? JS는 DOM 트리의 콘텐츠만 수정할 수 있으므로 필연적으로 DOM 변경이 발생하므로 렌더링 파이프라인의 첫 번째 단계부터 끝까지 시작되며 컴포지션 레이어에서 직접 시작되지는 않습니다. .

js로 수정된 스타일은 하드웨어 가속이 불가능하므로 어떻게 수정할 수 있나요? 애니메이션이나 전환과 같은 JS가 아닌 방법을 사용할 수 있습니다. js가 페이지를 완전히 차단해도 애니메이션이 계속 작동하는지 실험하고 확인할 수 있습니다.

        성능을 기록하고 렌더링 정지 문제를 해결하는 방법

페이지가 걸렸는지 안 걸렸는지 판단하려면 그냥 직접 해보고 안 걸렸다고 느낄 필요가 없습니다. 주관적인 고려에 기초한 정량적 데이터를 제공할 수는 없습니다. 작업에 포함된 데이터에서 시작하여 다른 사람을 설득해야 합니다. . 

1. 개발자 지표

특정 페이지를 로컬에서 여는 성능을 확인하려면 DevTools의 Lighthouse로 이동하세요. 

 성능 보고서를 생성하려면 페이지 로드 분석 버튼을 클릭하세요.

 첫 번째 그리기 시간 및 상호 작용 시간과 같은 성능 지표, 접근성 지표, 사용자 경험 지표, SEO 지표, PWA(프로그레시브 웹 애플리케이션) 등이 포함됩니다.

네이티브 렌더링 지연의 원인을 해결하고 싶은 경우. DevTools의 성능으로 이동하면 작업이 낮은 것부터 높은 것까지 명확하게 표시됩니다. 긴 작업을 나타내는 Long Task라는 단어를 볼 수 있습니다. 긴 작업의 정의는 50밀리초 동안 메인 스레드를 차단하는 것입니다. 또는 위의 작업. 긴 작업을 클릭하면 이 작업에서 수행된 작업을 자세히 볼 수 있습니다(예에서는 querySelectorAll이 너무 오래 걸리므로 최적화해야 함).

2. 실제 사용자 모니터링

위 내용은 개발 과정 중 일시적인 문제 해결에만 적합하며, 대상 장비는 귀하의 컴퓨터와 네트워크 상태뿐입니다. 온라인 상태가 된 프로젝트의 실제 성능이 다양한 네트워크 환경, 장치, 지리적 위치에 있는 사용자에 의해 어떻게 수행될지 알 수 없습니다. 따라서 실제 성과 지표를 알고 싶다면 다른 방법이 필요합니다.

성과가 좋은지 나쁜지 판단하려면 먼저 명확한 지표 이름을 정의해야 합니다. 그렇다면 성과를 판단하기 위해서는 어떤 지표가 필요한가?

 

 이러한 지표를 얻는 방법은 무엇입니까? 최신 브라우저에는 일반적으로 성능 API가 있어 자세한 성능 데이터를 많이 볼 수 있으며, 위 데이터 중 일부는 직접 사용할 수 없지만 몇 가지 기본 API를 통해 계산할 수 있습니다.

예를 들어, eventCounts - 이벤트 수, 메모리 - 메모리 사용량, 탐색 - 페이지 열기 방법, 리디렉션 수, 타이밍 - DNS 쿼리 시간, tcp 연결 시간, 응답 시간, DOM 구문 분석 및 렌더링 시간 등이 있음을 알 수 있습니다. 상호작용 시간 등

또한 성능에는performance.getEntries()와 같은 몇 가지 매우 유용한 API도 있습니다.

모든 리소스와 중요한 순간에 소요된 시간을 나열하는 배열이 반환됩니다. 그 중에는 first-paint-FP 및 first-contentful-paint-FCP 표시기가 있습니다. 특정 특정 성능 보고서만 찾으려면performance.getEntriesByName() 및performance.getEntriesByType()을 사용하여 필터링할 수 있습니다.

TTI(상호작용 시간) 표시기를 계산하는 방법에 대해 이야기해 보겠습니다.

  1. 먼저 위의performance.getEntries()를 통해 얻을 수 있는 First Contentful Paint 첫 번째 콘텐츠 그리기(FCP) 시간을 가져옵니다.
  2. 타임라인의 정방향으로 최소 5초 동안 지속되는 조용한 창을 검색합니다. 여기서 조용한 창은 다음과 같이 정의됩니다: 긴 작업 없음(장시간 작업, js는 50ms 이상의 작업 차단) 및 2개 이하의 네트워크 GET 요청이 처리되고 있습니다.
  3. 타임라인을 따라 역방향으로 Quiet Window 이전의 마지막 Long Task를 검색하며, Long Task가 발견되지 않으면 FCP 단계에서 실행을 중지합니다.
  4. TTI는 Quiet Window 이전의 마지막 긴 작업의 종료 시간입니다(긴 작업이 발견되지 않은 경우 FCP 값과 동일).

아마도 어려운 점은 사람들이 긴 작업을 수행하는 방법을 모른다는 것입니다. 성능 데이터를 모니터링하는 데 사용할 수 있는 PerformanceObserver라는 클래스가 있습니다. 긴 작업 정보를 얻으려면 EntryTypes에 longtask를 추가하세요. 또한 다른 성능 지표를 얻기 위해 더 많은 유형을 추가할 수도 있습니다. 자세한 내용은 이 클래스의 문서를 참조하세요. 다음은 longtask를 모니터링하는 예입니다.

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.

다양한 성능 지표를 계산하는 코드를 작성한 후(또는 기성 라이브러리를 직접 사용) 이를 사용자의 페이지에 묻어두고 사용자가 페이지를 열 때 성능 통계 백엔드에 보고할 수 있습니다.

JS를 최적화하는 방법

js에 대한 최적화 각도는 여러 가지가 있으며 이를 다양한 시나리오로 나누어야 하므로 기술 스택 선택, 멀티스레딩 및 v8의 관점에서 이야기해야 합니다. 

기술 스택 선택

1. 페이지 렌더링 솔루션 선택

1. CSR 브라우저 측 렌더링: 최근에는 React 및 Vue와 같은 스파 프런트엔드 프레임워크가 매우 인기가 있으며, 상태 기반 스파 애플리케이션은 빠른 페이지 전환을 달성할 수 있습니다. 그러나 그에 따른 단점은 모든 로직이 브라우저 측의 js에 있으므로 첫 번째 화면 시작 프로세스가 너무 길어진다는 것입니다.

2. SSR 서버 측 렌더링: spa(CSR 브라우저 측 렌더링)의 기본 렌더링 프로세스는 서버 렌더링(SSR)보다 길기 때문에 html 요청 -> js 요청 -> js 실행 -> js를 거치게 됩니다. 컨텐츠 렌더링 -> DOM 마운트 후 인터페이스 요청 -> 컨텐츠 업데이트. 서버 측 렌더링에는 html 요청 -> 페이지 콘텐츠 렌더링 -> js 요청 -> js 이벤트 추가 실행 단계만 필요합니다. 페이지 콘텐츠 렌더링의 경우 서버 측 렌더링은 SPA보다 훨씬 빠르며, 이는 사용자가 가능한 한 빨리 콘텐츠를 볼 것으로 예상되는 시나리오에 매우 적합합니다.

React의 nextjs 프레임워크나 vue의 nuxtjs 프레임워크를 사용할 수 있습니다. 이 프레임워크는 프런트엔드와 백엔드에서 동일한 코드 세트를 사용하여 서버 측 동형 렌더링을 달성할 수 있습니다. 핵심 원칙은 nodejs가 제공하는 서버 측 실행 환경과 가상 돔 추상화 계층이 제공하는 멀티 플랫폼 렌더링 기능입니다. 동일한 코드 세트를 사용하면 브라우저와 서버가 모두 렌더링할 수 있으며(서버는 html 텍스트를 렌더링함) 페이지가 두 번 점프할 때 첫 화면의 속도를 잃지 않고 여전히 스파 방법을 사용할 수 있습니다. 스파. 동시에 두 가지 주요 프레임워크의 SSR 성능도 지속적으로 최적화됩니다. 예를 들어 React18의 SSR에서 새로운 renderToPipeableStream API는 HTML을 스트리밍할 수 있고 시간이 많이 걸리는 작업을 건너뛰고 사용자가 볼 수 있는 Suspense 기능을 갖추고 있습니다. 메인 페이지가 더 빨라졌습니다. 또한 선택적 하이드레이션을 사용하여 게으른 및 서스펜스(클라이언트 측 렌더링과 동일)로 동기적으로 로드할 필요가 없는 구성 요소를 선택적으로 래핑하고, 기본 페이지의 대화형 시간을 최적화하고, ssr 분할에서 코드를 간접적으로 달성할 수 있습니다.

3. SSG 정적 페이지 생성: 예를 들어 React의 nextjs 프레임워크는 정적 사이트 생성을 지원하고 패키징 중에 구성 요소를 직접 실행하여 최종 HTML을 생성합니다. 정적 페이지는 런타임 없이 열 수 있어 최고의 열기 속도를 달성할 수 있습니다.

4. 앱 클라이언트 측 렌더링: 프런트 엔드 페이지가 앱에 배치된 경우 클라이언트는 서버 측 렌더링과 동일한 메커니즘을 구현할 수 있습니다. 이때 앱에서 페이지를 여는 것은 서버 측 렌더링과 유사합니다. . 또는 더 간단한 접근 방식은 프런트 엔드 스파 패키지를 즉시 열 수 있는 클라이언트 패키지에 넣는 것입니다. 실제로 가장 큰 속도 향상 점은 사용자가 앱을 설치할 때 프런트엔드 리소스도 다운로드한다는 것입니다.

2. 프론트엔드 프레임워크 선택

현대의 프론트엔드 개발 프로세스에서는 일반적으로 프레임워크 개발이 주저 없이 선택됩니다. 그러나 현재 또는 미래의 프로젝트가 복잡하지 않고 극도로 성능을 추구한다면 실제로 React 및 Vue와 같은 상태 기반 프레임워크를 사용할 필요는 없습니다. 상태 페이지만 수정하여 업데이트하는 개발 편의성을 누릴 수는 있지만, DX(개발자 경험) 향상 시 성능 비용도 발생합니다. 우선, 추가 런타임 도입으로 인해 js의 개수가 늘어났습니다. 둘째, 최소한 컴포넌트 레벨 렌더링이기 때문에, 즉 상태가 변경된 후 해당 컴포넌트가 전체적으로 다시 실행되므로 획득한 가상 DOM은 더 나은 브라우저 렌더링 성능을 얻기 위해 diff를 거쳐야 합니다. 이러한 추가 링크는 js 또는 jquery를 직접 사용하여 dom을 정확하게 수정하는 것만큼 빠르지 않다는 것을 의미합니다. 따라서 현재 또는 미래의 프로젝트가 복잡하지 않고 충분히 빠르고 가벼워지기를 원한다면 js 또는 jquery를 사용하여 직접 구현할 수 있습니다.

3. 프레임워크 최적화

React 프레임워크를 선택하는 경우 일반적으로 개발 프로세스 중에 몇 가지 추가 최적화를 수행해야 합니다. 예를 들어 종속성이 변경되지 않은 경우 useMemo를 사용하여 데이터를 캐시하고, 종속성이 변경되지 않은 경우 useCallback을 사용하여 함수를 캐시하고, 클래스 구성 요소의 shouldComponentUpdate를 사용하여 구성 요소를 업데이트해야 하는지 여부를 결정합니다. React는 변수 참조 주소의 변경 여부에 따라 내부적으로 업데이트 여부를 결정하기 때문에 두 객체나 배열 리터럴이 완전히 동일하더라도 두 개의 다른 값이라는 점에 유의해야 합니다.

또한 가능하다면 최신 버전을 계속 사용하도록 하세요. 일반적으로 새 버전은 성능을 최적화합니다.

예를 들어, React18은 긴 작업이 페이지 상호 작용을 차단하는 것을 방지하기 위해 작업 우선 순위 메커니즘을 추가합니다. 낮은 우선순위 업데이트는 높은 우선순위 업데이트(예: 사용자 클릭 및 입력)로 인해 중단되고, 낮은 우선순위 업데이트는 높은 우선순위 업데이트가 완료될 때까지 계속됩니다. 이러한 방식으로 사용자는 상호 작용할 때 응답이 시기 적절하다고 느낄 것입니다. useTransition 및 useDeferredValue를 사용하여 우선순위가 낮은 업데이트를 생성할 수 있습니다.

또한 React18은 일괄 업데이트도 최적화합니다. 과거에는 일괄 업데이트가 실제로 다음과 유사한 잠금 메커니즘을 통해 구현되었습니다.

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

update();
update();

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

 이렇게 하면 반응 외부에 잠금이 없기 때문에 수명 주기, 후크 및 반응 이벤트와 같은 고정된 장소에서만 일괄 업데이트를 사용하도록 제한됩니다. 또한 현재 매크로 작업과 독립적인 setTimeout 및 ajax와 같은 API를 사용하는 경우 내부 업데이트가 일괄적으로 업데이트되지 않습니다.

lock();

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

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

 React18의 일괄 업데이트는 우선순위를 기준으로 설계되었기 때문에 일괄 업데이트에 대해 React에서 지정한 위치에 있을 필요는 없습니다.

4. 프레임워크 생태 선택

프레임워크 자체 외에도 생태학적 선택도 성능에 영향을 미칩니다. vue의 생태는 일반적으로 상대적으로 고정되어 있지만, React의 생태는 매우 풍부합니다. 성능을 추구하려면 다양한 라이브러리의 특성과 원리를 이해해야 합니다. 여기서는 주로 전역 상태 관리 및 스타일 솔루션 선택에 대해 설명합니다.

상태 관리 라이브러리를 선택할 때, React-redux는 극단적인 조건에서 성능 문제가 발생할 수 있습니다. 참고로 우리는 redux가 아니라 React-redux를 이야기하고 있습니다. Redux는 그냥 일반적인 라이브러리일 뿐이고 매우 간단하고 다양한 곳에 사용할 수 있습니다. 성능에 대해 직접적으로 이야기하는 것은 불가능합니다. React-redux는 React가 Redux를 사용할 수 있도록 해주는 라이브러리입니다. Redux 상태는 매번 새로운 참조이기 때문에, React-redux는 상태에 의존하는 어떤 구성 요소를 업데이트해야 하는지 알 수 없습니다. 비교하려면 선택기를 사용해야 합니다. 이전 및 이후 값, 변경 여부. 전역 상태에 의존하는 컴포넌트의 각 셀렉터는 한 번씩 실행되어야 하는데, 셀렉터의 로직이 무거우거나 컴포넌트 수가 많으면 성능 문제가 발생한다. mbox를 사용해 볼 수 있습니다. 기본 원리는 vue와 동일하며, 가로채는 객체의 getter 및 setter를 기반으로 업데이트를 트리거하므로 어떤 구성 요소를 업데이트해야 하는지 자연스럽게 알 수 있습니다. 또한, zustand는 좋은 경험을 갖고 있어 적극 추천합니다. 역시 redux 기반이지만 사용이 매우 편리하고 많은 템플릿 코드 없이 컴포넌트 외부에서 사용할 수 있습니다.

스타일 솔루션 중 Styled-Component, Emotion 등 Runtime에서는 css-in-js 솔루션만 가능합니다. 그러나 절대적이지는 않습니다. 일부 CSS-in-js 라이브러리는 소품을 기반으로 스타일을 동적으로 계산하지 않을 때 런타임을 제거합니다. 스타일이 컴포넌트 소품을 기반으로 계산되는 경우 런타임이 필수적입니다. 컴포넌트 js를 실행할 때 CSS를 계산한 다음 스타일 태그를 추가합니다. 이는 두 가지 문제를 야기할 것입니다. 하나는 성능 비용이고, 다른 하나는 스타일 렌더링 시간이 js 실행 단계로 지연된다는 것입니다. css-in-js 이외의 솔루션(예: css, sass, less, stylus, tailwind)을 사용하여 이를 최적화할 수 있습니다. 여기서 가장 권장되는 것은 네트워크 수준 최적화에 대해 이야기할 때 언급했던 tailwind입니다. 런타임이 없을 뿐만 아니라 원자화로 인해 스타일을 완전히 재사용할 수 있으며 CSS 리소스가 매우 작습니다.

JS 멀티스레딩

우리는 js 작업이 페이지 렌더링을 차단한다는 것을 앞서 알고 있었지만 비즈니스에 긴 작업이 필요한 경우에는 어떻게 될까요? 대용량 파일 해시 등. 이때 우리는 또 다른 스레드를 시작하여 이 긴 작업을 실행하고 메인 스레드에 최종 결과를 알려줄 수 있습니다. 

웹 작업자 

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);
};

Web Worker는 이를 생성한 페이지 창인 스레드를 통해서만 액세스할 수 있습니다.

공유 근로자

Shared Worker는 다양한 창, iframe 및 작업자에서 액세스할 수 있습니다.

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);
  };
};

스레드 안전성 정보

Web Worker는 다른 스레드와의 통신 지점을 신중하게 제어하기 때문에 실제로 동시성 문제를 일으키기가 어렵습니다. 스레드로부터 안전하지 않은 구성 요소나 DOM에 액세스할 수 없습니다. 직렬화된 개체를 통해 스레드 안팎으로 특정 데이터를 전달해야 합니다. 따라서 코드에 문제를 일으키려면 정말 열심히 노력해야 합니다.

콘텐츠 보안 정책

워커에는 자신을 생성한 문서의 컨텍스트와는 다른 자체 실행 컨텍스트가 있습니다. 따라서 작업자는 문서의 콘텐츠 보안 정책에 따라 관리되지 않습니다. 예를 들어 문서는 이 http 헤더로 제어됩니다.

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

 이렇게 하면 페이지의 모든 스크립트가 eval()을 사용하는 것을 방지할 수 있습니다. 그러나 스크립트에서 작업자가 생성되면 작업자 스레드에서 eval()을 계속 사용할 수 있습니다. Worker에서 Content-Security-Policy를 제어하려면 Worker 스크립트의 http 응답 헤더에 이를 설정해야 합니다. 한 가지 예외는 작업자의 원본이 전역적으로 고유한 식별자(예: blob://xxx)인 경우 문서의 Content-Security-Policy를 상속한다는 것입니다.

데이터 전송

공유 메모리 주소가 아닌 기본 스레드와 작업자 스레드 간에 전달되는 데이터가 복사됩니다. 객체는 전달되기 전에 직렬화되고 수신되면 역직렬화됩니다. 대부분의 브라우저는 구조화된 복제 알고리즘을 사용하여 복사를 구현합니다.

V8 엔진 내부 최적화에 적응

V8 컴파일 파이프라인

  1. 환경 준비: V8은 먼저 코드의 런타임 환경을 준비합니다.이 환경에는 힙 공간과 스택 공간, 전역 실행 컨텍스트, 전역 범위, 내장 내장 함수, 확장 함수 및 호스트 환경에서 제공하는 개체가 포함됩니다. 메시지 루프 시스템. 전역 실행 컨텍스트와 전역 범위를 초기화합니다. 실행 컨텍스트에는 주로 변수 환경, 어휘 환경, this 및 범위 체인이 포함됩니다. var 및 function으로 선언된 변수는 변수 환경에 들어가게 되는데, 이 단계는 코드 실행 전에 이루어지므로 변수를 승격시킬 수 있습니다. const 및 let으로 선언된 변수는 스택 구조인 어휘 환경에 배치됩니다. {} 코드 블록에 들어오고 나갈 때마다 해당 변수는 스택에서 푸시되고 팝되고, 그 변수는 스택에서 팝됩니다. 접근할 수 없으므로 const와 let은 어휘 효과를 갖습니다.
  2. 이벤트 루프 시스템 구성: 메인 스레드는 실행을 위해 작업 큐에서 작업을 지속적으로 읽어야 하므로 루프 이벤트 메커니즘을 구성해야 합니다.
  3. 바이트코드 생성: V8은 런타임 환경을 준비한 후 먼저 코드에 대한 어휘 및 구문 분석(Parser)을 수행하고 AST 및 범위 정보를 생성한 후 AST 및 범위 정보를 Ignition이라는 인터프리터에 입력하고 변환합니다. 바이트코드로 변환합니다. 바이트코드는 플랫폼 독립적인 중간 코드입니다. 여기서 바이트코드를 사용하면 최적화된 기계어 코드로 컴파일할 수 있다는 장점이 있으며, 바이트코드를 캐싱하면 기계어 코드를 캐싱하는 것보다 메모리가 많이 절약됩니다. 바이트코드 생성 시 파싱이 지연됩니다. V8은 모든 코드를 한 번에 컴파일하지 않습니다. 함수 선언을 만나면 함수 내부의 코드를 즉시 파싱하지 않고 최상위 함수에서만 AST와 바이트코드를 생성합니다.
  4. 바이트코드 실행: V8의 인터프리터는 바이트코드를 직접 실행할 수 있습니다. 바이트코드에서 소스 코드는 Ldar, Add 및 기타 어셈블리와 유사한 명령어로 컴파일되어 가져오기, 구문 분석 명령어, 실행 명령어, 데이터 저장 등과 같은 명령어를 구현할 수 있습니다. . 일반적으로 스택 기반과 레지스터 기반의 두 가지 유형의 인터프리터가 있습니다. 스택 기반 인터프리터는 스택을 사용하여 함수 매개변수, 중간 계산 결과, 변수 등을 저장합니다. 레지스터 기반 가상 머신은 레지스터를 사용하여 매개변수 및 중간 계산 결과를 저장합니다. 대부분의 인터프리터는 Java 가상 머신, .Net 가상 머신 및 초기 V8 가상 머신과 같은 스택 기반이며 현재 V8 가상 머신은 레지스터 기반 설계를 채택합니다.
  5. JIT just-in-time 컴파일: 바이트코드를 직접 실행할 수는 있지만 시간이 오래 걸린다.코드 실행 속도를 높이기 위해 V8은 인터프리터에 모니터를 추가한다.바이트코드 실행 중에 특정 코드 조각이 반복되는 것으로 확인됩니다. 여러 번 실행하면 모니터링에서 이 코드를 핫 코드로 표시합니다.

         특정 코드 조각이 핫 코드로 표시되면 V8은 바이트코드를 최적화 컴파일러 TurboFan에 전달하고 최적화 컴파일러는 바이트코드를 바이너리 코드로 컴파일한 다음 컴파일된 바이너리 코드에서 컴파일을 수행합니다. 최적화된 바이너리 기계어 코드의 실행 효율성이 크게 향상됩니다. 이 코드가 나중에 실행되면 V8은 최적화된 바이너리 코드에 우선순위를 부여하는데, 이러한 설계를 JIT(Just-In-Time Compile)라고 합니다.

          그러나 JavaScript는 정적 언어와 달리 유연한 동적 언어이므로 런타임에 변수의 유형과 객체의 속성을 수정할 수 있습니다. 그러나 최적화 컴파일러에 의해 최적화된 코드는 고정된 유형만 대상으로 할 수 있습니다. 변수가 동적으로 수정되면 최적화된 기계 코드는 유효하지 않은 코드가 됩니다. 이때 최적화 컴파일러는 역최적화 작업을 수행해야 하며 다음 번 실행 시 해석 및 실행을 위해 인터프리터로 대체됩니다. 추가적인 역최적화 프로세스는 기존의 바이트코드 직접 실행보다 느립니다.

위의 컴파일 파이프라인을 통해 js가 동일한 코드 조각을 여러 번 반복적으로 실행한다는 것을 알 수 있는데, JIT의 존재로 인해 속도가 매우 빠릅니다(java 및 java와 같은 정적으로 강력한 유형의 언어와 동일한 수준). 씨#). 그러나 전제는 유형과 객체 구조를 마음대로 변경할 수 없다는 것입니다. 예를 들어, 다음 코드입니다.

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

V8 엔진 스토리지 개체 ​​최적화

JS 객체는 힙에 저장되며 문자열을 키 이름으로 사용하는 사전과 유사하며 모든 객체를 키 값으로 사용할 수 있으며 키 이름을 통해 키 값을 읽고 쓸 수 있습니다. 그러나 V8에서는 객체 저장소를 구현할 때 주로 성능 고려 사항으로 인해 사전 저장소를 완전히 사용하지 않았습니다. 사전은 비선형 데이터 구조이기 때문에 해시 계산 및 해시 충돌로 인해 순차적으로 저장된 데이터 구조에 비해 질의 효율성이 떨어지며, V8은 저장 및 검색 효율성을 높이기 위해 복잡한 저장 전략을 채택합니다. 순차 저장 구조는 선형 목록 및 배열과 같은 연속적인 메모리 조각이고, 비선형 구조는 일반적으로 연결된 목록 및 트리와 같은 비연속 메모리를 차지합니다.

객체는 일반 속성과 정렬 속성으로 구분됩니다. 숫자 속성은 정렬 속성이라고 하는 오름차순으로 자동 정렬되며 개체의 모든 속성 시작 부분에 배치됩니다. 문자열 속성은 생성된 순서대로 일반 속성 내에 배치됩니다.

V8 내에서는 이 두 속성을 저장하고 액세스하는 성능을 효과적으로 향상시키기 위해 두 개의 선형 데이터 구조를 사용하여 각각 정렬 속성과 일반 속성, 즉 요소와 속성의 두 가지 숨겨진 속성을 저장합니다.

이 두 가지 조건이 충족되면: 객체가 생성된 후 새 속성이 추가되지 않고, 객체가 생성된 후 속성이 삭제되지 않으며, V8은 각 객체에 대해 숨겨진 클래스를 생성하고 객체에 다음을 가리키는 맵 속성 값이 있습니다. 그것. 객체의 히든 클래스는 객체에 포함된 모든 속성과 객체의 시작 메모리에 상대적인 각 속성 값의 오프셋이라는 두 가지 점을 포함하여 객체의 일부 기본 레이아웃 정보를 기록합니다. 이렇게 하면 속성을 읽을 때 일련의 프로세스가 필요 없으며 오프셋을 직접 가져오고 메모리 주소를 계산할 수 있습니다.

하지만 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은 이에 대한 인라인 캐시를 생성합니다. 예를 들어 다음 코드는 다음과 같습니다.

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)를 유지합니다. 피드백 벡터는 여러 항목으로 구성되며 각 항목을 슬롯이라고 하며, 위 코드에서 V8은 읽기 기능에 의해 실행된 중간 데이터를 피드백 벡터의 슬롯에 순차적으로 씁니다.

코드의 반환 object.a는 호출 지점입니다. 객체 속성을 읽기 때문에 V8은 읽기 함수의 피드백 벡터에서 이 호출 지점에 슬롯을 할당하고 각 슬롯에는 슬롯 인덱스( 슬롯 인덱스)가 포함됩니다. ), 슬롯 유형(type), 슬롯 상태(state), 히든 클래스의 주소(map), 속성의 오프셋을 V8에서 다시 읽기 함수를 호출하고 return object.a를 실행하면 오프셋을 검색하게 됩니다. 그러면 V8은 해당 슬롯에 있는 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);
}

 코드가 이렇게 되면 각 루프에서 읽은 두 객체의 모양이 다르기 때문에 히든 클래스도 다르다는 것을 알 수 있습니다. V8이 두 번째 객체를 읽을 때 슬롯의 히든 클래스가 읽고 있는 클래스와 다르다는 것을 발견하므로 새로운 히든 클래스와 속성 값 메모리 오프셋을 슬롯에 추가합니다. 이때 슬롯에는 2개의 히든클래스와 오프셋이 있게 됩니다. 객체의 속성을 읽을 때마다 V8은 이를 하나씩 비교합니다. 읽고 있는 객체의 히든 클래스가 슬롯의 히든 클래스 중 하나와 동일한 경우 적중 히든 클래스의 오프셋이 사용됩니다. 상응하는 정보가 없으면 새 정보도 해당 슬롯에 추가됩니다.

  • 슬롯에 히든 클래스가 1개만 포함된 경우 이 상태를 단일형( monomorphic)이라고 합니다.

  • 슬롯에 2~4개의 히든 클래스가 포함된 경우 이 상태를 다형성( polymorphic)이라고 합니다.

  • 한 슬롯에 히든클래스가 4개 이상 있을 경우 이 상태를 슈퍼상태( magamorphic)라고 합니다.

단형성의 성능이 가장 좋다고 볼 수 있으므로 더 나은 성능을 얻기 위해 여러 번 실행되는 함수에서 객체를 수정하거나 여러 객체를 읽는 것을 피하려고 노력할 수 있습니다.

이전에 React17 버전을 봤을 때 "createElement 대신 _jsx를 사용하는 이유"에 대한 공식 설명에서 createElement의 몇 가지 단점에 대해 언급했습니다. createElement는 "다형성이 매우 높으며" V8 수준. 사실 이 글을 이해하시면 저 문장이 무슨 뜻인지 이해가 되실 텐데요, 실제로 글에서 언급한 슈퍼스테이트이기 때문에 관계자가 왜 createElement를 최적화하기 어렵다고 했는지 이해가 될 것입니다. createElement 함수는 페이지 내에서 여러 번 호출되지만, 받아들이는 컴포넌트 props와 기타 매개변수가 다르기 때문에 인라인 캐시가 많이 생성되므로 "고다형성(하이퍼스테이트)"이라고 합니다. (그러나 적어도 이 point_jsx는 아직 해결되지 않은 것 같지만 이를 실현할 수 있다는 점은 여전히 ​​매우 강력합니다)


드디어 끝났습니다! 만들기가 쉽지 않으니 재인쇄시 꼭 말씀해주세요. 엄지손가락을 치켜세워주세요! ! !

추천

출처blog.csdn.net/YN2000609/article/details/132408002