Firestore 성능 향상 및 비용 절감을 위한 간단한 방법

18748 단어 firebaserxjs
이 글은 이미 Firebase's Firestore에 익숙한 사람들을 위해 준비한 것이다.
최근에 나는 간단한 검색 캐시를 사용하면Firebase의Firestore의 성능을 향상시키고 원가를 낮출 수 있다는 것을 깨달았다.

  • 편집: 생산 과정에서 한동안 사용한 후에 저는 이런 기술이 주로 성능 개선이고 원가에 큰 영향을 미치지 않는다는 것을 발견했습니다(퍼스트 스토어의 지속성을 사용했기 때문에 원가를 낮추었기 때문일 수 있습니다).

  • 문제.
    Firebase의Firestore는 처음에 속도가 매우 빨랐다. 데이터를 자동으로 연결하는 것을 지원하지 않기 때문이다. (즉 SQL 연결). 그러나 흔히 볼 수 있는 해결 방법은 Observable를 사용하여 다음과 같은 작업을 수행하는 것이다. (예시 rxFire docs)
    import { collectionData } from 'rxfire/firestore';
    import { getDownloadURL } from 'rxfire/storage';
    import { combineLatest } from 'rxjs';
    import { switchMap } from 'rxjs/operators';
    
    const app = firebase.initializeApp({ /* config */ });
    const citiesRef = app.firestore().collection('cities');
    
    collectionData(citiesRef, 'id')
      .pipe(
        switchMap(cities => {
          return combineLatest(...cities.map(c => {
            const ref = storage.ref(`/cities/${c.id}.png`);
            return getDownloadURL(ref).pipe(map(imageURL => ({ imageURL, ...c })));
          }));
        })
      )
      .subscribe(cities => {
        cities.forEach(c => console.log(c.imageURL));
      });
    
    이 예에서는 도시를 불러오고 돌아오는 도시마다 이미지 데이터를 가져와야 합니다.
    이런 방법을 사용하면 한 조회가 몇 십 개의 조회로 변할 때 조회 속도가 매우 빨리 느려진다.내가 나의 비영리 조직을 위해 개발한 작은 응용 프로그램에서 시간의 추이 (새로운 기능이 증가함에 따라) 에 따라 모든 검색이 누적될 것이다. 갑자기 당신은 5-30초를 기다리고 어떤 페이지를 불러오기를 기다릴 것이다.만약 네가 페이지 사이를 빠르게 왔다갔다한다면, 어떤 지연도 사람을 매우 화나게 할 것이다.

    "I just loaded this data a second ago, why does it need to load everything again?"


    내가 하고 싶은 것은 조회 데이터를 일정 시간 캐시하는 것이다. 이렇게 하면 만약 누군가가 몇 페이지 사이를 왔다갔다 내비게이션을 한다면 조회 데이터를 신속하게 다시 사용할 수 있다.그러나 자세히 고려하지 않은 상황에서 캐시와 같은 실현은 시간이 필요할 것 같고 상당히 많은 복잡성을 증가시켰다.Firestore의 지속성을 사용해 보았습니다. 중복 조회, 캐시 데이터를 자동으로 제거하고 성능을 향상시키기를 바랐지만, 제가 원하는 효과는 나타나지 않았습니다. (이것은 어느 정도 원가를 낮추었지만 의외로 성능을 떨어뜨렸습니다.)
    더 좋은 캐시를 만드는 것은 매우 쉽다는 사실이 증명되었다.

    솔루션
    모든 관찰자가 구독 데이터를 취소한 후에도 간단한 조회 캐시를 구현했습니다.구성 요소가 새로운 검색을 실행할 때, 나는Firestore를 즉시 호출하지 않고, 관련 검색이 생성되었는지 확인하기 위해 검색 캐시를 검사합니다.만약 그렇다면, 나는 기존의 조회를 다시 사용할 것이다.그렇지 않으면, 나는 새로운 조회를 만들고 그것을 나중에 사용할 수 있도록 캐시할 것이다.
    코드:
    import { Observable, Subject } from 'rxjs';
    import stringify from 'fast-json-stable-stringify';
    import { delay, finalize, shareReplay, takeUntil } from 'rxjs/operators';
    
    /** Amount of milliseconds to hold onto cached queries */
    const HOLD_CACHED_QUERIES_DURATION = 1000 * 60 * 3; // 3 minutes
    
    export class QueryCacheService {
      private readonly cache = new Map<string, Observable<unknown>>();
    
      resolve<T>(
        service: string,
        method: string,
        args: unknown[],
        queryFactory: () => Observable<T>,
      ): Observable<T> {
        const key = stringify({ service, method, args });
    
        let query = this.cache.get(key) as Observable<T> | undefined;
    
        if (query) return query;
    
        const destroy$ = new Subject();
        let subscriberCount = 0;
        let timeout: NodeJS.Timeout | undefined;
    
        query = queryFactory().pipe(
          takeUntil(destroy$),
          shareReplay(1),
          tapOnSubscribe(() => {
            // since there is now a subscriber, don't cleanup the query
            // if we were previously planning on cleaning it up
            if (timeout) clearTimeout(timeout);
            subscriberCount++;
          }),
          finalize(() => { // triggers on unsubscribe
            subscriberCount--;
    
            if (subscriberCount === 0) {
              // If there are no subscribers, hold onto any cached queries
              // for `HOLD_CACHED_QUERIES_DURATION` milliseconds and then
              // clean them up if there still aren't any new
              // subscribers
              timeout = setTimeout(() => {
                destroy$.next();
                destroy$.complete();
                this.cache.delete(key);
              }, HOLD_CACHED_QUERIES_DURATION);
            }
          }),
          // Without this delay, very large queries are executed synchronously
          // which can introduce some pauses/jank in the UI. 
          // Using the `async` scheduler keeps UI performance speedy. 
          // I also tried the `asap` scheduler but it still had jank.
          delay(0),
        );
    
        this.cache.set(key, query);
    
        return query;
      }
    }
    
    /** 
     * Triggers callback every time a new observer 
     * subscribes to this chain. 
     */
    function tapOnSubscribe<T>(
      callback: () => void,
    ): MonoTypeOperatorFunction<T> {
      return (source: Observable<T>): Observable<T> =>
        defer(() => {
          callback();
          return source;
        });
    }
    
    이렇게 캐시를 사용할 수 있습니다.
    export class ClientService {
      constructor(
        private fstore: AngularFirestore,
        private queryCache: QueryCacheService,
      ) {}
    
      getClient(id: string) {
        const query =
          () => this.fstore
            .doc<IClient>(`clients/${id}`)
            .valueChanges();
    
        return this.queryCache.resolve(
          'ClientService', 
          'getClient', 
          [id], 
          query
        );
      }
    }
    
    현재 ClientService#getClient() 방법을 호출할 때 방법 매개 변수와 표지부호는 조회 공장 함수와 함께 조회 캐시 서비스에 전달됩니다.검색 캐시 서비스는 fast-json-stable-stringify 라이브러리를 사용하여 검색의 표지 정보를 문자열화하고 이 문자열을 키로 사용하여 검색의 관찰 가능한 정보를 캐시합니다.캐시 질의에 앞서 observable은 다음과 같이 수정됩니다.

  • 미래의 구독자들이 최신 결과를 즉시 얻을 수 있도록 shareReplay(1)을 추가하였으며, 마지막으로 이 조회를 구독한 구독자가 구독을 취소한 후에도 밑바닥Firestore 데이터에 대한 구독을 유지하도록 하였다.
  • 추적 조회의
  • 구독자는 마지막 구독자가 구독을 취소한 후에 타이머를 설정하여 밑에 있는Firestore 데이터의 구독을 자동으로 취소하고 사용자가 정의한 설정 시간대(현재 3분 사용 중) 후에 캐시를 지웁니다.
  • delay(0)은 구독자에게 asyncSchedular을 강제로 사용하도록 한다.나는 이것이 캐시된 대형 데이터 집합을 불러올 때 UI의 민첩성을 유지하는 데 도움이 된다는 것을 알았다. (그렇지 않으면 UI가 대형 데이터를 동기화해서 불러오려고 시도하고, 이로 인해 말더듬이/jank를 초래할 수 있다.)
  • 이 캐시는 조회마다 HOLD_CACHED_QUERIES_DURATION을 설정할 수 있도록 한층 더 업데이트할 수 있다.

    결론
    이런 간단한 캐시는 같은 문서가 한 번 또 한 번 빠르게 다시 불러오는 것을 방지할 수 있다면 원가를 낮출 수 있다.만약 조회가 Date개의 매개 변수를 사용하여 구축된다면 잠재적인'문제'가 있을 것이다.이 경우, 검색의 매개 변수로 new Date()을 조심스럽게 사용해야 합니다. 이것은 호출할 때마다 검색과 관련된 캐시 키를 변경하기 때문입니다. (기본적으로 캐시가 사용되는 것을 막을 수 있습니다.)Date을 작성하여 만들 수 있습니다(예를 들어 startOfDay(new Date())규범 date-fns).
    도움이 되었으면 좋겠습니다.

    좋은 웹페이지 즐겨찾기