TL;DR: Keep it simple.
Unless you have backing evidence (from a profiler, or network inspector) that loading these polyfills are a problem, I suggest you don't really worry about it. I'm not so worried about size, and that's because you can always take advantage of minification, gzipping, browser caching. If you worry about cache invalidation, it can always be solved with cache busting.
The promise library is just ~30KB unminified. The fetch polyfill is just ~8KB unminified. Minified, they can even be smaller. If you are able to pre-process your HTML, you can use usemin. Usemin lets you write scripts and styles in special comment blocks. Running the task during build, it will process the HTML, compressing those blocks into one file.
<head>
...
<!-- build:js dist/js/polyfills.js -->
<script src="path/to/promise.js"></script>
<script src="path/to/fetch.js"></script>
<!-- other polyfills -->
<!-- endbuild -->
...
</head>
<!-- becomes -->
<head>
<!-- contains a concatenated, minified bundle of the files -->
<script src="dist/js/polyfills.js"></script>
</head>
If you rather take the RequireJS optimizer route, the optimizer doesn't handle dynamic dependencies. Additionally, since either libs look like they aren't AMD modules, use RequireJS's shim config. Then let the parts of your code that need fetch or promises require them.
// Config
require.config({
shim:{
promise: {
path: 'path/to/promise.js',
exports: 'Promise',
},
fetch: {
path: 'path/to/fetch.js',
exports: 'fetch',
deps: ['promise'],
}
}
});
// Usage
define(['fetch'], function(fetch){
fetch(...).then(...);
});
Now I never did say it wasn't possible with RequireJS to dynamically load dependencies, or at least in the browser-run version (I usually call it "runtime" version). Since the config is executed on the browser during runtime as opposed to written statically for the optimizer, you can dynamically add configs or modules.
var myShims = {
// your other shims, if any.
};
// Have a utility function that either exposes the native API as a module
// or shims the library as a module, pointing to the polyfill.
function polyfillOrNative(globalName, path, deps){
if(window[globalName]){
// Note that this is a named module. So you have to require it by name.
// For the sake of brevity, I just used the global name as module name.
// Otherwise, names are usually small letters, and separated by dashes.
define(globalName, deps, function(){
return window[globalName];
});
} else {
myShims[globalName] = {
path: path,
exports: globalName,
deps: deps,
}
}
}
polyfillOrNative('Promise', 'path/to/fallback/promise.js', []);
polyfillOrNative('fetch', 'path/to/fallback/fetch.js', ['Promise']);
require.config({
shim: myShims,
});
// You can now depend on fetch and/or promise as modules.
define(['fetch'], function(fetch){
fetch(...).then(...);
});
But then, what I wrote here introduced unnecessary logic into your codebase. It's much easier to just add your polyfills up in the head or just hard-written dependencies than having this convoluted logic in your code. I'd say weigh your options, because in the long run, JS is really hard to maintain without discipline.