stephanrauh / ngx-extended-pdf-viewer

A full-blown PDF viewer for Angular 16, 17, and beyond
https://pdfviewer.net
Apache License 2.0
450 stars 167 forks source link

Optimization: Caching library #2337

Open YaromichSergey opened 1 month ago

YaromichSergey commented 1 month ago

I have a question-request. How to cache/optimize library sources and loads. We use pdf library to view pdf. We have component which re-initialize a lot of times during user session. And every time we need to load js files. How to optimize this process? Is it possible to load only first time? We understand that js files caching and next time it's load from browser cache. But we still need time to process js. What is your recommendation for best optimization, reusabillity component? Thanks in advance

stephanrauh commented 1 month ago

pdf.js is removed from memory when you destroy the Angular component. Instead, you can simply hide the <ngx-extended-pdf-viewer> component. You just have to make sure the component is visible before modifying the [src] attribute. Maybe you need to add a timeout like so:

this.showPDF = true;
setTimeout(() => this.src="/assets/my-favorite-pdf-file.pdf", 0);
<ngx-extended-pdf-viewer [style.hidden]="!showPDF" [src]="src">

Note that this is pseudo-code - I didn't try it. If it works, please tell me, so I can fix the errors of my pseudo-code for the benefit of other users.

YaromichSergey commented 1 month ago

It's not what we are looking for. We need something global in case of saving viewer. I'm trying to initialize it with ComponentFactoryResolver to save instance of library but it still re-launch(pdf-worker) and others and it's a bit harder to use. Main problem to avoid load library(from source from cache what ever) and avoid executing JS each time it need. Best behavior: load library(all source what needs), cache them and each time we invoke it be ready to show document as it waits for them. We need to understand if it possible. By our investigating it looks like not, at least with how we using library.

stephanrauh commented 1 month ago

There are checks preventing loading the viewer-*.js and the pdf-*.js twice. The check regarding pdf-*.js seems to be broken, but at least the viewer shouldn't be loaded twice. However, when I tested it, none of these file were loaded twice. Can you confirm that?

Remains the worker. That's probably the most annoying file because it's so huge. After reading the api.js file I believe it's possible to cache the worker, but I didn't manage to wrap my head around it yet.

How urgent is your issue? Are you ready to dive into the source code yourself? At the moment, I'm trying to catch up after my vacations, so I appreciate every help I can get!

YaromichSergey commented 1 month ago

We are really interesting in it. Unfortunately I can't help with it. I tested again and can see that viewer-*.js and pdf-.js loads only first time. We are waiting for caching workers(pdf.worker-) and all you can do as soon as you can implement it. If you can mention in some way with ready state of this it will be great. Thank you in advance

stephanrauh commented 1 month ago

Issue #2329 is doing the exact opposite: they noticed I've implemented a memory leak, so the garbage collector can't remove the JavaScript resources. I'll tackle #2329 first. It might make your issue worse.

Here's something you can try: you can load the JavaScript files yourself. Actually you can even load the worker file yourself, but that means the worker is executed in the main thread, so it reduces the rendering performance. It depends on the use-case which tradeoff is worse.

  private loadViewer(): void {
    globalThis['ngxZone'] = this.ngZone;
    this.ngZone.runOutsideAngular(() => {
      this.needsES5().then((needsES5) => {
        const viewerPath = this.getPdfJsPath('viewer', needsES5);
        const script = this.createScriptElement(viewerPath);
        document.getElementsByTagName('head')[0].appendChild(script);
      });
    });
  }

  private loadPdfJs() {
    globalThis['ngxZone'] = this.ngZone;
    this.ngZone.runOutsideAngular(() => {
      if (!globalThis['pdfjs-dist/build/pdf']) {
        this.needsES5().then((needsES5) => {
          if (needsES5) {
            if (!pdfDefaultOptions.needsES5) {
              console.log(
                "If you see the error message \"expected expression, got '='\" above: you can safely ignore it as long as you know what you're doing. It means your browser is out-of-date. Please update your browser to benefit from the latest security updates and to enjoy a faster PDF viewer."
              );
            }
            pdfDefaultOptions.needsES5 = true;
            console.log('Using the ES5 version of the PDF viewer. Your PDF files show faster if you update your browser.');
          }
          if (this.minifiedJSLibraries && !needsES5) {
            if (!pdfDefaultOptions.workerSrc().endsWith('.min.mjs')) {
              const src = pdfDefaultOptions.workerSrc();
              pdfDefaultOptions.workerSrc = () => src.replace('.mjs', '.min.mjs');
            }
          }
          const pdfJsPath = this.getPdfJsPath('pdf', needsES5);
          if (pdfJsPath.endsWith('.mjs')) {
            const src = pdfDefaultOptions.workerSrc();
            if (src.endsWith('.js')) {
              pdfDefaultOptions.workerSrc = () => src.substring(0, src.length - 3) + '.mjs';
            }
          }
          const script = this.createScriptElement(pdfJsPath);
          script.onload = () => {
            if (!(globalThis as any).webViewerLoad) {
              this.loadViewer();
            }
          };
          document.getElementsByTagName('head')[0].appendChild(script);
        });
      } else if (!(globalThis as any).webViewerLoad) {
        this.loadViewer();
      }
    });
  }

  private createScriptElement(sourcePath: string): HTMLScriptElement {
    const script = document.createElement('script');
    script.async = true;
    script.type = sourcePath.endsWith('.mjs') ? 'module' : 'text/javascript';
    this.pdfCspPolicyService.addTrustedJavaScript(script, sourcePath);
    return script;
  }

In your application, you can simplify the code a lot. You don't need all the options a general-purpose library needs.

stephanrauh commented 3 days ago

I've got an idea how to improve performance in version 21. If everything goes according to plan, I'm going to move the code loading and managing the base library into a service, so it doesn't have to be loaded every time you navigate from page to page.

YaromichSergey commented 3 days ago

It will be great. Looking forward for your news. Thanks for replying here. As for the error with old version of chrome with new version. Do you have a plan to fix it? Or just throw error with event?. Unfortunately we still have old users with old browsers and need to support them. For now we have workaround with window.onerror but we understand that it's not the best idea. Could you share you thoughts?

stephanrauh commented 3 days ago

Which bug are you referring to? Maybe Promise.withResolvers? I think you might be able to polyfill it - but to my surprise, I haven't found a polyfill yet.

YaromichSergey commented 3 days ago

Yes, about this bug. I tried to polyfill it and it's isn't working. I tried to add to angular code something like

function promiseWithResolvers() {
  let resolve;
  let reject;
  const promise = new Promise(
    (res, rej) => {
      // Executed synchronously!
      resolve = res;
      reject = rej;
    });
  return {promise, resolve, reject};
}

But it didn't work as expected(nothing changed). Don't know is it read code and can work(you can test it)

stephanrauh commented 2 days ago

That's pretty much the approach I'd use, just slightly different - maybe that's why it's not working?

I'd add these lines to the module file or to the constructor of the component using <ngx-extended-pdf-viewer>:

Promise.promiseWithResolvers() = function() {
  let resolve;
  let reject;
  const promise = new Promise(
    (res, rej) => {
      // Executed synchronously!
      resolve = res;
      reject = rej;
    });
  return {promise, resolve, reject};
}
stephanrauh commented 2 days ago

BTW, if everything fails, there's always the option to add the attribute [forceUsingLegacyES5]="true".

stephanrauh commented 1 day ago

Have a look at version 21.0.0-alpha.0. I'd be very surprised if it's not full of bugs - but it works surprisingly good, and jumping between the demos of the showcase seems to be considerably faster.

YaromichSergey commented 3 hours ago

I have already tested it and have this moments:

  1. assetsFolder stopped work(it add full passed string to current domain for example: http://localhost:4200, and as assetsFolder = 'http://localhost:4201/assets/pdf' in result will be - 'http://localhost:4200/http://localhost:4201/assets/pdf'. In previous version everything is ok.
  2. Could you delete logs pls(from console) or provide a way how to hide them?
  3. As for openning process - something changed with toolbar. After starting loading - shows more buttons(initial), after load - shows another ones(We are not using them at all and hide it, for now logic changed). If it part of the logic - it's ok, we will fix on our side.

As for loading process, it's hard to say that it loads faster, need to re-test it again after your fixes. Thanks in advance