Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Force some polyfill into de bundle #2969

Closed
1 task done
coluccini opened this issue Sep 21, 2017 · 26 comments
Closed
1 task done

Force some polyfill into de bundle #2969

coluccini opened this issue Sep 21, 2017 · 26 comments

Comments

@coluccini
Copy link
Contributor

  • I have searched the issues of this repository and believe that this is not a duplicate.

I need to support some older browsers, so I would like to always include some polyfills (Intl, Object.assign and Promise) in the app bundle and just execute them if need (I've tried to do it conditionally, but it fails in a lot of situations), but I can't figure out the way to do it. Has anybody did this o has an idea on how can I do it?

@markozxuu
Copy link
Contributor

markozxuu commented Sep 21, 2017

What browsers are ? I thought that babel solved the problem of compatibility.

@coluccini
Copy link
Contributor Author

As I understand, Babel transpile es6 code to es5. That doesn't means that all browser support all functions. That's why they also provide babel-polyfill. The think is I don't want to load all babel-polyfill and I don't want the polyfills to overwrite native functions on browser that already support them

@timneutkens
Copy link
Member

This is an interesting one, most people include it in the head using _document.js

@coluccini
Copy link
Contributor Author

Yes. I was using polyfill.io services, which is great and work fine. But the request to the service it's pretty slow even for browser that doesn't need any polyfill. So I think that if I'm going to penalize every user in favor of those with old browser, having the polyfill code inside of the bundle is a better option than adding an extra/external request before the bundle get loaded. What do you think?

NOTE: I've try to dynamically add the polyfill.io script when needed (using a small JS code that appends the script tag -with async=false- inside head, like the one that loads Google Analytics or similars) but for some reason in some browsers the next.js code was still executing before the polyfill is loaded so it failed 😞

@radeno
Copy link
Contributor

radeno commented Oct 13, 2017

Same issue for me. I need to add some polyfills but not language polyfills as Promises or Array functions but Window/Render polyfills. Specifically IntersectionObserver and Smoothscoll.

Both require access to window, but it is not available in server side rendering. So i need to put it into separate script file. Any way how to force Webpack create new js file?
I don't want to use polyfill.io which is great service, but our project can't depend on 3rd party service.

@coluccini
Copy link
Contributor Author

It is pretty hacky, but the option it worked the best for me is to dynamically load the polyfill with a synchronous XMLHttpRequest() before next's js are loaded. I use a code like the following:

(function(undefined) {
  if (!('Promise' in this)) {
    var src = 'https://cdn.polyfill.io/v2/polyfill.min.js?features=Promise&flags=always&unknown=polyfill';
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (xhr.readyState == XMLHttpRequest.DONE) {
          var f = new Function(xhr.responseText);
          f();
        }
    }
    xhr.open('GET', src, false);
    xhr.send();
  }
})
.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});

If you don't want to use Polyfill service, you can always get the code from them, save it in a local file and then loaded from some static url/CDN (polyfill is not a code that usually change)

@radeno
Copy link
Contributor

radeno commented Oct 13, 2017

@coluccini yes it is pretty hacky, but no other options because NextJS has not easy hooks to change generated files (entry and CommonPlugin).

Where did you put this code ? Just for example in _document.js at begin of file?

@coluccini
Copy link
Contributor Author

@radeno, exactly: in _document.js inside <Head>

@radeno
Copy link
Contributor

radeno commented Oct 17, 2017

@coluccini does it work for you? I get and error XMLHttpRequest is not defined

@coluccini
Copy link
Contributor Author

Yes. But I'm not trying to executing directly in next. I'm print that code on SSR:

const polyfill = `THE_CODE`;

export default class baseDocument extends Document {
  render() {
    return (
      <html lang={locale}>
        //...
        <body>
          //...
          <script dangerouslySetInnerHTML={{ __html: polyfill }} />
          <NextScript />
        </body>
      </html>
    );
  }
}

@radeno
Copy link
Contributor

radeno commented Oct 17, 2017

I see, so you inlining requested script content, is it neccessary? Isn't this easier to use script tag with src attribute?

export default class baseDocument extends Document {
  render() {
    return (
      <html lang={locale}>
        //...
        <body>
          //...
          <script
            type="text/javascript"
            src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Promise&flags=always&unknown=polyfill"
          />
          <NextScript />
        </body>
      </html>
    );
  }
}

or just put script tag into component. By testing performance is same and NextJS script is executed right after.
I made Self hosted Docker container of Polyfill.io together with Nginx request caching based on URL and User agent. In some cases it should faster than official because lower usage. https://hub.docker.com/r/smikino/polyfill-service/

@coluccini
Copy link
Contributor Author

Yes, you can do it that way for sure (you should remove &flags=always from the url, with it polyfill.io will add the polyfill even if the user's browser don't need it).

There are two bad thing about this approach:

  1. You're adding a blocking request before requesting your app's js affecting every users, including those that doesn't need the polyfill (usually the vast majority) and not just users that need it.
  2. I've seen a few user agent not getting any polyfill from polyfill.io service even they do need it.

If you are ok with those issues, this is the way to go.

@stoikerty
Copy link
Contributor

If Next.js had a way of allowing the control of ReactDOM.render/ReactDOM.hydrate to be called asynchronously, then it would be possible to use polyfill.io effectively with the following pattern.

//
// You can achieve non-blockin polyfills with polyfill.io using the following
// event-pattern, assuming you can control when to start rendering.
//

// ·······························································
// 1) Consumer entry point, waits for polyfills before being executed.
//    (Single App or multiple separate components like Header, Footer, Modal)
// ---------------------------------------------------------------

// Each consumer listens for the custom event.
window.addEventListener('polyfills-loaded', function () {
  // Start up the application where imports and everything else can use polyfills
  require('./startApp');
}, false);

// ·······························································
// 2) Script at the bottom of the page, right before the body closing tag.
//    Loads the polyfills without blocking html render
// ---------------------------------------------------------------

// Initialize a custom event so that consumers can listen to it
var event = document.createEvent('Event');
event.initEvent('polyfills-loaded', true, true);

// Define what should happen once the polyfills have finished loading
window.__whenPolyfillsAreLoaded = function () {
  console.log('Polyfills are ready. Starting up...');
  // Dispatch the event to run the application components
  window.dispatchEvent(event);
};

// Add a script tag to the page to start loading the polyfills
const polyfillScript = document.createElement('script');
polyfillScript.src = 'https://cdn.polyfill.io/v2/polyfill.js?callback=__whenPolyfillsAreLoaded';
polyfillScript.type = 'text/javascript';
polyfillScript.async = true;
document.head.appendChild(polyfillScript);

@redbmk
Copy link
Contributor

redbmk commented Nov 3, 2017

I ran into an issue when creating an Outlook add-in, where window.history functions seem to be stripped, so Next crashes in the first file, which gets added before any children in Head. So I can't do something like

<Head>
  <script "/static/fix-window-history-for-outlook-add-in.js" />
</Head>

because that generates something like

<head>
  <script "/some-next-script-that-relies-on-window-history-working-properly.js" />
  <script "/static/fix-window-history-for-outlook-add-in.js" />
</head>

I'm not finding a good way to put the file at the beginning of the <head> tag.

@coluccini
Copy link
Contributor Author

That’s weird, cause Next usually puts their scripts at the end of the body tag. Have you try to use a _document.js file to generate the base html and the put your script ahead of the next ones?

@redbmk
Copy link
Contributor

redbmk commented Nov 3, 2017

Ah, no you're right - I misunderstood how <link rel="preload"... worked. I had the impression that since those preloads were at the top of <head> that that's where they were being loaded. It looks like the scripts required for office mess with history, so I just needed to load the office scripts first, then the history fix, then the Next scripts. Sorry to spam here, but yes, using a custom _document.js is the way to go for this situation.

@clemencov
Copy link

To prevent blocking request in modern browsers you can add nomodule attribute (https://developer.mozilla.org/ru/docs/Web/HTML/Element/script#attr-nomodule) to <script /> tag, so browsers that support ES6 modules (if they supports modules — no polyfills is needed for them) will not even make a request.

Ex:

<script
  src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Promise"
  nomodule="nomodule"
/>

@timneutkens
Copy link
Member

You can now use https://github.com/zeit/next.js/blob/canary/server/build/webpack.js#L48 in canary we still need to document this but if someone wants to make an example using it that'd be great.

// next.config.js
const path = require('path')
module.exports = {
     clientBootstrap: [
          path.join(__dirname, 'polyfill.js')
    ]
}

^ in this case you need to have polyfill.js in your main directory obviously.

@coluccini
Copy link
Contributor Author

That's great @timneutkens! I'll try it and, if I succeed, I'll add an example :)
I think we can close this ticket now 🍾

@timneutkens
Copy link
Member

Thanks @coluccini!

@designspin
Copy link

@coluccini , did you get anywhere with a documented example?

I am trying to load an intersection observer polyfill using the clientBootstrap, but the trouble is I don't know how to hold now execution until my polyfil is completed loading. Anybody done something similar?

@coluccini
Copy link
Contributor Author

Sorry, I still didn't try this new configuration option. But as I understand if you achieve to bootstrap your polyfill file like proposed, it should be executed before next.js runs.
Seeing your code on #3449 I would said the problem is that you are running the polyfill asynchronously. I think you should have the polyfill code directly in the file you are bootstraping

@designspin
Copy link

@coluccini, Thanks for your reply, I was kind of hoping I could dynamically import the polyfil only when it is actually needed.

@coluccini
Copy link
Contributor Author

To do that I run this code inside the <head> of pages/_document.js:

(function(undefined) {
  if ('IntersectionObserver' in global) {
    var src = ROUTE_TO_POLYFILL_FILE;
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (xhr.readyState == XMLHttpRequest.DONE) {
          var f = new Function(xhr.responseText);
          f();
        }
    }
    xhr.open('GET', src, false);
    xhr.send();
  }
})
.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {});

I'm using it on a site with a lot of user and seams to work pretty well.

@designspin
Copy link

@coluccini , thanks I think i'll have to give that a go.

@arunoda
Copy link
Contributor

arunoda commented Jan 14, 2018

Hello,

We are going to remove clientBootstrap in the future. Simply because we can simply use our custom webpack config.
Checkout this example: #3568

@lock lock bot locked as resolved and limited conversation to collaborators Jan 14, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants