• Najnowsze pytania
  • Bez odpowiedzi
  • Zadaj pytanie
  • Kategorie
  • Tagi
  • Zdobyte punkty
  • Ekipa ninja
  • IRC
  • FAQ
  • Regulamin
  • Książki warte uwagi

Sposób na uniknięcie freezów strony - asynchronizm 'ciężkich' funkcji

Object Storage Arubacloud
+1 głos
388 wizyt
pytanie zadane 3 września 2021 w JavaScript przez Oskar Szkurłat Bywalec (2,780 p.)

Cześć,
Pracuję na projekcie, który korzysta ze sporych danych, co przekłada się freezowanie strony, przy np. przygotowaniu danych do exportu do pdf/xlsx, czy też zaznaczaniu danych w tabeli, rozwijaniu itp. Używam w projekcie devexpress, ale już mam wrażenie, że ustawiłem wszystko co mogłem do optymalizacji, ale wciąż strona zamiera na czas obliczeń.

Rozwiązaniem mojego problemu byłoby znalezienie sposobu na zapewnienie asynchroniczności do moich funkcji, tzn. chciałbym poinformować przeglądarkę, że ma obliczać funkcję tylko, gdy ma "zapas" czasu, tak aby strona nie freezowała się, a obliczenia się wykonywały w tle (ja sobie zapodam wtedy efekt loadingu na komponent).

Jakie są wasze sposoby na to? W głównej mierze zabójcami mojej strony są forEach i map, myślałem żeby je ustawić, jako asynchroniczne i zmusić do spowolnienia czasowego (np. 1ms na iteracje jako 'timeout'), ale mega nie byłbym zadowolony z takiego rozwiązania. Poza tym w przypadku niektórych funkcji (np. wyciąganie danych z dataGrid devexpress) nie mam żadnego wpływu na iteracje w nich zachodzące, bo to biblioteki.

Próbowałem wrzucić np. funkcję do Promise i wyzwalać go w momencie zakończenia obliczeń, ale bez rezultatów, wywołanie promisa też freezuje stronę. Funkcja jest wywoływana po prostu w przycisku na onClick(). Przykład poniżej:

async function onExportPdf(e) {
      try {
        await new Promise((resolve, reject) => {
          const doc = new jsPDF()
          grid.getDataSource().store().load().done().then((data) => {
            const conditions = grid.getCombinedFilter('returnDataField')
            let outDataSource = new ArrayStore(data)
            outDataSource
              .load({ filter: conditions })
              .done((filteredData => {
                doc.autoTable({
                  body: props.options.worksheetExport.getDetails(filteredData),
                  columns: props.options.worksheetExport.columns,
                })
              }))
            doc.save('myFile.pdf')
            e.cancel = true
            resolve()
          })
        })
      } catch (reject) {
        console.error(reject)
      }
    }

Z góry dzięki :)

1 odpowiedź

+3 głosów
odpowiedź 3 września 2021 przez ScriptyChris Mędrzec (190,190 p.)

Próbowałeś ciężkie obliczenia podzielić na wątki?

komentarz 3 września 2021 przez Oskar Szkurłat Bywalec (2,780 p.)
edycja 3 września 2021 przez Oskar Szkurłat

Nie, nigdy z nich nie korzystałem. Mógłbym poprosić o jakiś przykład prosty, jako wzorzec, bo nie wiem jak miałbym przekazać zmienne do Worker w innym pliku? Wolałbym, żeby to było mimo wszystko w jednym. Na razie coś takiego kombinuję, ale nie działa:

function onExportPdf(e) {
      const worker = new Worker(
        window.addEventListener('message', function (event) {
          switch (event.data) {
            case 'start':
              const doc = new jsPDF()
              grid.getDataSource().store().load().done().then((data) => {
                const conditions = grid.getCombinedFilter('returnDataField')
                let outDataSource = new ArrayStore(data)
                outDataSource
                  .load({ filter: conditions })
                  .done((filteredData => {
                    doc.autoTable({
                      body: props.options.worksheetExport.getDetails(filteredData),
                      columns: props.options.worksheetExport.columns,
                    })
                  }))
                doc.save(props.options.worksheetExport.fileName + '.pdf')
                e.cancel = true
                this.clear() //Tak można?
              })
              break
            default:
              console.error('onExportPdf worker error')
          }
        }, false)
      )

      worker.postMessage('start')
    }

Dodam jeszcze, że korzystam z Reacta, bo widzę, że to ma tutaj znaczenie.

komentarz 3 września 2021 przez ScriptyChris Mędrzec (190,190 p.)

Do konstruktora Worker powinieneś przekazać URL do skryptu, który ma być wykonany jako Worker, a nie docelowy kod (wywołanie window.addEventListener, który teraz przekazałeś). Możesz ewentualnie stworzyć Worker w formie inline. Przykłady przesyłania danych są np. tutaj.

1
komentarz 3 września 2021 przez Oskar Szkurłat Bywalec (2,780 p.)
przywrócone 3 września 2021 przez Oskar Szkurłat

Dobra udało mi się zrobić workera i skomunikować. Przez to, że pracuję w React musiałem przenieść workers do public folderu. Tylko w jaki sposób miałbym przekazać komponenty z funkcji (tu e, grid) i zaimportować bibliotekę w Workerze?
 

  const onExportPdfWorker = new Worker(`/workers/onExportPdf.js`)

  useEffect(() => {
    onExportPdfWorker.onmessage = (e) => {
      if (e != null && e.data != null) {
        console.info('Worker reported ', e.data)
      }
    }
  }, [onExportPdfWorker])


    function onExportPdf(e) {
      onExportPdfWorker
        .postMessage(
          {
            msg: 'execute',
            e: e,
            grid: 1,
            jsPDF: 1
          }
        )
    }
const workerName = 'onExportPdf'

this.onmessage = async (event) => {
  if (event && event.data && event.data.msg === 'execute') {
    this.postMessage(executeAndReport(event.data))
  }
}

function executeAndReport(d) {
  //Expected: e, grid, jsPDF from receivedData
  if (d.e == null || d.grid == null || d.jsPDF == null) {
    return 'Code 400: Missing correct data at worker ' + workerName
  }

  console.info(d.e, d.grid, d.jsPDF)

  return 'Code 200: No error from worker ' + workerName
}

Gdy przekazuję w obiekcie event.data.grid lub event.data.e dostaję komunikat błędu: 

DataCloneError: Failed to execute 'postMessage' on 'Worker': function getCoalescedEvents() { [native code] } could not be cloned.

Jeżeli chodzi o biblioteki to w przypadku import zwraca mi, że próbowano użyć import w module, a w przypadku require określa, że nie zna tej metody.

komentarz 3 września 2021 przez Oskar Szkurłat Bywalec (2,780 p.)
edycja 3 września 2021 przez Oskar Szkurłat
Czyli jak rozumiem, niemożliwe jest przekazanie funkcji, jako parametr do Workersa? No i zgodnie z dokumentacją Worker nie jest wykonywalny, więc nie obsłuży bibliotek (importScripts() zwraca taki właśnie błąd). Więc chyba rozwiązanie z wątkiem mija się z celem, skoro wszystko mam obliczyć przed nim - bo nie przyjmie funkcji, żeby liczyć w sobie ? Próbowałem też przekazać funkcję, jako JSON i odparsować po stronie workera, ale też nie działa ;<

//Edit: udało się przekazać funkcję w bardzo niefajny sposób, ale nie testowałem wystarczająco póki co :)
po stronie reacta: String(jsPDF)
po stronie workera: eval(`var jsPDF = ${d.jsPDF}`); jsPDF()
komentarz 3 września 2021 przez ScriptyChris Mędrzec (190,190 p.)

A czemu przekazujesz funkcję? Jeśli korzystasz z biblioteki w React, to powinno dać się jej używać bezpośrednio w Workerze, przesyłając do Workera dane, które ma policzyć (za pośrednictwem biblioteki) i odbierając od niego wynik.

Jeżeli chodzi o biblioteki to w przypadku import zwraca mi, że próbowano użyć import w module,

W jaki sposób używasz import? Jeśli to składnia modułów ECMAScript, to Firefox obecnie nie wspiera jej w workerach (choć wygląda na to, że pracują nad tym). Możesz spróbować zbundlować sobie skrypt workera (docs, przykład) z dołączoną biblioteką dla Reacta i serwować ją osobno, a wtedy funkcja importScripts w workerze powinna to załadować.

a w przypadku require określa, że nie zna tej metody.

require to metoda wbudowana w Node, nie jest natywnie dostępna w przeglądarce. Chyba, że korzystasz z biblioteki do obsługi modułów CommonJS lub bundlujesz je np. webpack-iem.

Więc chyba rozwiązanie z wątkiem mija się z celem

JavaScript jest jednowątkowy i jeśli aplikacja faktycznie zwalnia z powodu nadmiernych obliczeń, a nie słabego kodu lub niemożności jego optymalizacji, to oddelegowanie obciążających obliczeń do osobnego wątku jest sensownym rozwiązaniem.

komentarz 3 września 2021 przez ScriptyChris Mędrzec (190,190 p.)

//Edit: udało się przekazać funkcję w bardzo niefajny sposób, ale nie testowałem wystarczająco póki co :)
po stronie reacta: String(jsPDF)
po stronie workera: eval(`var jsPDF = ${d.jsPDF}`); jsPDF()

Zamiast eval spróbuj użyć konstruktora Funkcji, bo eval jest zły (chyba, że naprawdę nie masz wyjścia).

komentarz 6 września 2021 przez Oskar Szkurłat Bywalec (2,780 p.)
edycja 6 września 2021 przez Oskar Szkurłat

Niestety aktualnie pomimo, że działa mi worker dla własnego kodu, nie potrafię rozwiązać problemu z bibliotekami. Początkowo używałem zgodnie z tym co wyżej przedstawiłem new Worker(), ale teraz strikte dla reacta użyłem hooka useWorker. W obu przypadkach jest ten sam rezultat. Próbowałem zrobić bundlery, z czego, jak rozumiem one działają trochę na zasadzie "wybuilduj i korzystaj z buidlowanego pliku" - takie konwertowanie kodu wykonywanego JS w strukturę 'html'. Dla parcela generował błąd, który zgodnie z forami jest błędem parcela, który od 2 lat nie został rozwiązany. :( a webpack pomimo konfiguracji, nic nie zmieniał. Teraz spróbowałem zrobić Blob i jego umieścić w url Workera, tak jak wcześniej sugerowałeś. Jednak nawet w przypadku użycia bloba, konsola zwraca błąd Uncaught (in promise) w przypadku użycia jakiejkolwiek biblioteki wewnątrz workera. 

Aktualny kod:

const blob = new Blob([JSON.stringify(
    (data, conditions) => {
      const doc = new jsPDF()
      let outDataSource = new ArrayStore(data)
      outDataSource
        .load({ filter: conditions })
        .done((filteredData => {
          doc.autoTable({
            body: props.options.worksheetExport.getDetails(filteredData),
            columns: props.options.worksheetExport.columns,
          })
        }))
      doc.save(props.options.worksheetExport.fileName + '.pdf')
    }
  )], { type: 'application/json' })

const [exportPdfWorker] = useWorker(blob, {
    remoteDependencies: [
      "https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.3.1/jspdf.umd.min.js"
    ]
  })
  const exportPdfWorkerHandler = (e) => {
    grid.getDataSource().store().load().done().then((data) => {
      const conditions = grid.getCombinedFilter('returnDataField')
      const fcn = async () => {
        await exportPdfWorker(data, conditions)
      }
      fcn()
      e.cancel = true
    })
  }

...onClick: (e) => exportPdfWorkerHandler(e)...

Gdy próbowałem przekazać np. funkcję jsPDF, jako string do workera i potem odszyfrować, to również były błędy z wywołaniem jej.

komentarz 6 września 2021 przez ScriptyChris Mędrzec (190,190 p.)

webpack pomimo konfiguracji, nic nie zmieniał

W jakim sensie nic nie zmieniał? Nie tworzył osobnego pliku z bundlem skryptu dla workera, gdzie był już wstawiony kod zewnętrznej biblioteki?

Jednak nawet w przypadku użycia bloba, konsola zwraca błąd Uncaught (in promise) 

Jaki to konkretnie błąd i którego fragmentu kodu dotyczy?

useWorker(blob, {
    remoteDependencies: [
      "https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.3.1/jspdf.umd.min.js"
    ]
  })

Z tego co widzę w dokumentacji useWorker-a, to w pierwszym parametrze musisz przekazać tam funkcję, a nie bloba - blob z tej funkcji jest tworzony pod spodem (klik, klik).

komentarz 6 września 2021 przez Oskar Szkurłat Bywalec (2,780 p.)

W jakim sensie nic nie zmieniał? Nie tworzył osobnego pliku z bundlem skryptu dla workera, gdzie był już wstawiony kod zewnętrznej biblioteki?

Tak już z tym mieszam i się męczę, że się zgubiłem już... ;x ale tak webpacket nie chcial mi wypluć dir bez błędów (było ich mnóstwo, jeden za drugim), stąd się poddałem z tym bundlem. 

Jaki to konkretnie błąd i którego fragmentu kodu dotyczy?

Zwraca to właśnie wywołanie workera, dotyczy tego, że worker nie ma dostępu do bibliotek (z tego co rozumiem, bo Uncaught ReferenceError: jspdf__WEBPACK_IMPORTED_MODULE_7__ is not defined). Całą strukturę błędu wrzucam poniżej:

A ja próbowałem do workera już zgodnie z biblioteką przekazać jako CDN i jako lokalne, kod:

const [exportPdfWorker] = useWorker(
    (data, conditions) => {
      const doc = new jsPDF()
      let outDataSource = new ArrayStore(data)
      outDataSource
        .load({ filter: conditions })
        .done((filteredData => {
          doc.autoTable({
            body: props.options.worksheetExport.getDetails(filteredData),
            columns: props.options.worksheetExport.columns,
          })
        }))
      doc.save(props.options.worksheetExport.fileName + '.pdf')
    }, {
    localDependencies: () => ['jspdf', 'jspdf-autotable', 'devextreme/data/array_store']
  })

  const exportPdfWorkerHandler = (e) => {
    grid.getDataSource().store().load().done().then((data) => {
      const conditions = grid.getCombinedFilter('returnDataField')
      const fcn = async () => {
        await exportPdfWorker(data, conditions)
      }
      fcn()
      e.cancel = true
    })
  }

Z tego co widzę w dokumentacji useWorker-a

Też to zauważyłem, ale w obu przypadkach był ten sam błąd. Ustawiłem już na funkcję, a nie Blob.

komentarz 6 września 2021 przez ScriptyChris Mędrzec (190,190 p.)
Jak możesz, to udostępnij minimalną wersję całej apki (tzn. niezbędny kod do sprawdzenia tego workera z zewnętrzną biblioteką), żeby można to było przetestować. Może uda mi się to jakoś zdebugować.
komentarz 6 września 2021 przez Oskar Szkurłat Bywalec (2,780 p.)

Wybacz, że tyle to zajęło, ale wyodrębnienie tego z softu było czasochłonne :)
Link

komentarz 7 września 2021 przez ScriptyChris Mędrzec (190,190 p.)

Jest jakiś problem z zależnościami w tej bibliotece useWorker.

Dlatego jako workaround, na podstawie Twojego kodu, zaimplementowałem tworzenie workera, który ma swoje zależności i jest prawie osobno bundlowany. Całość działa, tylko że bottleneck-iem jest zapisywanie PDF-a. Problem polega na tym, że metoda jsPDF.save korzysta pod spodem z libki file-saver i ona robi jakąś magię na DOM-ie (coś z Canvas-em) do zapisywania plików. Niestety, worker nie ma dostępu do DOM-u, więc o ile proces tworzenia PDF-a na podstawie dostarczonych danych worker ogarnia po swojej stronie, to już do zapisu trzeba dane przesłać do głównego wątku i tam dopiero zapisać (co chwilę trwa).

Można by spróbować do workera wrzucić libkę JSDOM, która zasymuluje DOM i w ten sposób całość zapisu PDF oddelegować do workera. Nie chciało mi się już w to bawić, ale myślę, że powinieneś sobie dać z tym radę na podstawie istniejącej integracji.

Próbowałem pushować poprawiony kod do Twojego repo, ale nie mam do tego uprawnień. Więc albo możesz mi je na chwilę dać (nick ten sam co tutaj) albo wrzucę na swój GitHub i sobie stamtąd całość pobierzesz.

komentarz 7 września 2021 przez Oskar Szkurłat Bywalec (2,780 p.)
edycja 7 września 2021 przez Oskar Szkurłat
Oznaczyłem Cię na githubie, jako collaborator, to powinieneś móc pushować na moim repozytorium (napisane jest, że oczekuje zaproszenie dla Ciebie).
komentarz 7 września 2021 przez ScriptyChris Mędrzec (190,190 p.)

Ok, kod jest na branchu feature/implement-worker. Bundlowanie workera jest uruchamiane komendą npm run prepare-worker. Zależności dla workera dodajesz w pliku src/worker/deps.js. Jednocześnie te, które mają być dostępne w skrypcie workera musisz dodać do obiektu self.WorkerExport i potem w skrypcie do tego namespace się odnosić.

 
komentarz 9 września 2021 przez ScriptyChris Mędrzec (190,190 p.)

@Oskar Szkurłat, i jak - udało Ci się rozwiązać problem? 

1
komentarz 10 września 2021 przez Oskar Szkurłat Bywalec (2,780 p.)

Wybacz, że teraz nie odpisywałem, po prostu musiałem się zająć innymi tematami ;x. Od następnego tygodnia znowu będę siadał do tego :) ale dziękuję w ogóle ślicznie za dotychczasową pomoc ;) mam nadzieję, że mi się uda bez zadawania już pytań. Jak skończę i będzie działać coś więcej, to podrzucę rozwiązanie tutaj na forum dla potomnych.

Podobne pytania

+1 głos
1 odpowiedź 176 wizyt
+1 głos
0 odpowiedzi 145 wizyt
pytanie zadane 14 maja 2019 w JavaScript przez BT101 Stary wyjadacz (12,540 p.)
+1 głos
2 odpowiedzi 639 wizyt
pytanie zadane 9 lipca 2020 w JavaScript przez Greeenone Pasjonat (16,100 p.)

92,555 zapytań

141,404 odpowiedzi

319,557 komentarzy

61,940 pasjonatów

Motyw:

Akcja Pajacyk

Pajacyk od wielu lat dożywia dzieci. Pomóż klikając w zielony brzuszek na stronie. Dziękujemy! ♡

Oto polecana książka warta uwagi.
Pełną listę książek znajdziesz tutaj.

Akademia Sekuraka

Kolejna edycja największej imprezy hakerskiej w Polsce, czyli Mega Sekurak Hacking Party odbędzie się już 20 maja 2024r. Z tej okazji mamy dla Was kod: pasjamshp - jeżeli wpiszecie go w koszyku, to wówczas otrzymacie 40% zniżki na bilet w wersji standard!

Więcej informacji na temat imprezy znajdziecie tutaj. Dziękujemy ekipie Sekuraka za taką fajną zniżkę dla wszystkich Pasjonatów!

Akademia Sekuraka

Niedawno wystartował dodruk tej świetnej, rozchwytywanej książki (około 940 stron). Mamy dla Was kod: pasja (wpiszcie go w koszyku), dzięki któremu otrzymujemy 10% zniżki - dziękujemy zaprzyjaźnionej ekipie Sekuraka za taki bonus dla Pasjonatów! Książka to pierwszy tom z serii o ITsec, który łagodnie wprowadzi w świat bezpieczeństwa IT każdą osobę - warto, polecamy!

...