국제화 호출 속도를 최대 5-1000배 향상

문맥



모든 것은 2년 전에 시작되었습니다. 나는 다른 언어를 처리하기 위해 i18n 모듈이 필요한 처음부터 작성된 대규모 소셜 네트워크를 위한 새로운 PWA를 작업하고 있었습니다. 모듈은 다음을 수행해야 했습니다.
  • 보간을 처리합니다.
  • PLURAL 및 SELECT 표현식을 처리합니다.

  • 가벼워야 합니다(PWA이므로 제한된 대역폭으로 실행해야 함).

  • 빠르게 실행합니다(일부 사용자는 저사양 기기를 사용했습니다).

  • 그리고 그것이 소름 끼치는 곳입니다. 가능한 유일한 라이브러리는 Google Closure MessageFormat이었습니다. 저가형 장치에서는 그렇게 빠르지 않았고 번들에 큰 영향을 미쳤습니다. 그래서 저는 성능을 염두에 두고 저만의 글을 쓰기로 했습니다.

    현재까지도 문제는 여전히 i18n 라이브러리와 동일하므로 다른 것보다 5~1000배 빠른 1kb i18n 라이브러리를 오픈 소스💋Frenchkiss.js로 공개했습니다.
    성능 최적화에 대한 여정을 저와 함께 하세요.

    👉 Time to speed up your webapp for mobile devices!



    🤷 i18n 모듈은 어떻게 작동하나요?



    내부적으로는 일부 i18n 모듈이 호출할 때마다 변환을 다시 처리하여 성능이 저하됩니다.

    다음은 번역 기능 내에서 발생할 수 있는 일의 예입니다(Polyglot.js의 정말 단순화된 순진한 버전).

    const applyParams = (text, params = {}) => {
      // Apply plural if exists
      const list = text.split('||||');
      const pluralIndex = getPluralIndex(params.count);
      const output = list[pluralIndex] || list[0];
    
      // Replace interpolation
      return output.replace(/%\{\s*(\w+)\s*\}/g, ($0, $1) =>  params[$1] || '');
    }
    
    applyParams('Hello %{name} !', {
      name: 'John'
    });
    // => Hello John !
    

    즉, 각 번역 호출에서 텍스트를 분할하고, 복수형 인덱스를 계산하고, RegExp를 만들고, 지정된 매개변수가 있는 경우 지정된 매개변수로 모든 항목을 바꾸고 결과를 반환합니다.

    그다지 큰 문제는 아니지만 각 렌더링/필터/지시 호출에서 여러 번 수행해도 괜찮습니까?

    👉 It's one of the first things we learn when building app in react, angular, vuejs or any other framework : avoid intensive operations inside render methods, filters and directives, it will kill your app !



    일부 i18n 라이브러리가 더 잘 작동하고 있습니다!



    다른 사람들은 상황을 상당히 최적화하고 있습니다. 예를 들어 Angular, VueJs-i18n, Google Closure가 있습니다.

    그들은 그것을 어떻게 하고 있습니까? 실제로 그들은 문자열을 한 번만 구문 분석하고 다음 호출에서 처리하기 위해 opcode 목록을 캐시합니다.

    opcode에 익숙하지 않은 경우 기본적으로 처리할 지침 목록이며 이 경우 번역을 작성하기 위한 것입니다. 다음은 번역에서 생성된 opcode의 가능한 예입니다.

    [{
      "type": "text",
      "value": "Hello "
    }, {
      "type": "variable",
      "value": "name"
    }, {
      "type": "text",
      "value": " !"
    }]
    

    결과를 인쇄하는 방법:

    const printOpcode = opcodes => opcodes.map(code => (
      (code.type === 'text') ? code.value :
      (code.type === 'variable') ? (params[code.value] || '') :
      (code.type === 'select') ? printOpCode( // recursive
        params.data[params[code.value]] || params.data.other
      ) :
      (code.type === 'plural') ? printOpCode( // recursive
        params.list[getPluralIndex(params[code.value])] || params.list[0]
      ) :
      '' // TODO not supported ?
    )).join('');
    

    이 유형의 알고리즘을 사용하면 opcode를 생성하는 첫 번째 호출에 더 많은 시간이 할당되지만 저장하고 다음 호출에서 더 빠른 성능을 위해 재사용합니다.
  • 문자열을 분할하지 않습니다.
  • 집중적인 정규식 작업을 수행하지 않습니다.
  • opcode를 읽고 결과를 함께 병합합니다.

  • 글쎄요! 그러나 더 나아가는 것이 가능합니까?


    🤔 어떻게 속도를 높일 수 있을까요?



    💋Frenchkiss.js은 한 단계 더 나아가 번역을 네이티브 함수로 컴파일합니다. 이 함수는 매우 가볍고 순수하여 Javascript가 쉽게 처리할 수 있습니다JIT compile it.

    어떻게 작동합니까?



    매우 간단합니다. 실제로 다음을 수행하는 문자열에서 함수를 작성할 수 있습니다.

    const sum = new Function('a', 'b', 'return a + b');
    
    sum(5, 3);
    // => 8
    

    For further informations, take a look at Function Constructor (MDN).



    주요 논리는 여전히 opcode 목록을 생성하는 것이지만 번역을 생성하는 데 사용하는 대신 추가 프로세스 없이 번역을 반환하는 최적화된 함수를 생성하는 데 사용합니다.

    실제로 가능한 것은 간단한 보간 구조와 SELECT/PLUTAL 표현식 때문입니다. 기본적으로 일부 삼항으로 반환됩니다.

    const opCodeToFunction = (opcodes) => {
      const output = opcodes.map(code => (
        (code.type === 'text') ? escapeText(code.value) :
        (code.type === 'variable') ? `params[${code.value}]` :
        (code.type === 'select') ? ... :
        (code.type === 'plural') ? ... :
        '' // TODO Something wrong happened (invalid opcode)
      ));
    
      // Fallback for empty string if no data;
      const result = output.join('+') || "";
    
      // Generate the function
      return new Function(
        'arg0',
        'arg1',
        `
        var params = arg0 || {};
        return ${result};
      `);
    });
    

    ⚠️ 참고: 동적 함수를 빌드할 때 사용자 입력을 이스케이프 처리하여 피하십시오XSS injection!

    더 이상 고민하지 않고 생성된 함수를 살펴보겠습니다(참고: 실제 생성된 함수는 조금 더 복잡하지만 아이디어를 얻을 수 있습니다).

    보간 생성 함수



    // "Hello {name} !"
    function generated (params = {}) {
      return 'Hello ' + (params.name || '') + ' !';
    }
    

    By default, we still fallback to empty string to avoid printing "undefined" as plain text.



    표현식 생성 함수 선택



    // "Check my {pet, select, cat{evil cat} dog{good boy} other{{pet}}} :D"
    function generated (params = {}) {
      return 'Check my ' + (
        (params.pet == 'cat') ? 'evil cat' :
        (params.pet == 'dog') ? 'good boy' :
        (params.pet || '')
      ) + ' :D';
    }
    

    We don't use strict equality to keep supports for numbers.



    복수형 생성 함수



    // "Here {N, plural, =0{nothing} few{few} other{some}} things !"
    function generated (params = {}, plural) {
      const safePlural = plural ? { N: plural(params.N) } :{};
    
      return 'Here ' + (
        (params.N == '0') ? 'nothing' :
        (safePlural.N == 'few') ? 'few' :
        'some'
      ) + ' things !';
    }
    

    We cache the plural category to avoid re-fetching it in case of multiple checks.




    🚀 결론



    생성된 함수를 사용하여 우리는 다른 것보다 5배에서 1000배 더 빠르게 코드를 실행할 수 있었으며, 중요한 경로를 렌더링할 때 RegExp, 분할, 맵 작업을 수행하지 않고 가비지 수집기 일시 중지도 방지했습니다.

    Benchmark

    마지막으로 가장 좋은 소식은 GZIP 크기가 1kB에 불과하다는 것입니다!

    If you're searching for a i18n javascript library to accelerate your PWA, or your SSR, you should probably give 💋Frenchkiss.js a try !

    좋은 웹페이지 즐겨찾기