greensock / GSAP

GSAP (GreenSock Animation Platform), a JavaScript animation library for the modern web
https://gsap.com
19.83k stars 1.72k forks source link

How can I tree-shake CSSPlugin when writing a library? #374

Closed tonix-tuft closed 4 years ago

tonix-tuft commented 4 years ago

Hello,

I am using gsap in some modules related to CSS stuff and animations in a utility library called js-utl (https://github.com/tonix-tuft/js-utl). Some of the functions requiring gsap are:

./js-utl/src/modules/css-in-js.js

...
import { gsap } from "gsap";

...
export function transform(element, ...transforms) {
  gsap.set(element, Object.assign({}, ...transforms));
}

./js-utl/src/modules/animations.js

...
import { gsap } from "gsap";

...
export function fadeIn(node, options) {
  const opt = options || {};
  const secs = millisecToSec(opt.millisec || 300);
  const css = opt.css || {};
  gsap.fromTo(
    node,
    secs,
    {
      opacity: 0,
      display: css.display || "block",
      ...(opt.fromCSS || {})
    },
    {
      opacity: 1,
      ...(opt.toCSS || {}),
      onComplete: () => {
        opt.onComplete && opt.onComplete();
      }
    }
  );
}

I then use this js-utl library as a dependency of my other libraries, e.g. pigretto has a dependency on js-utl (https://github.com/tonix-tuft/pigretto).

However, pigretto only uses three functions exported by js-utl (namely isArray, isUndefined and isEmpty) and none of them have anything to do with gsap (pigretto is not a CSS/animation lib but only uses some functionality of js-utl which does not require gsap).

Then, when I generate the Webpack production build of pigretto, tree shaking works as expected for the three functions isArray, isUndefined and isEmpty, which are the only functions exported by js-utl that get bundled (I have double-checked it and e.g. ctypeDigit of ./js-utl/src/modules/core.js does not get bundled as it is not used by pigretto, as well as transform and fadeIn).

CSSPlugin, however, is bundled together with pigretto's code, though I wouldn't expect it to be.

That's, as far as I understand, because you implicitly register the plugin in your gsap/index.js file:

import { gsap, Power0, Power1, Power2, Power3, Power4, Linear, Quad, Cubic, Quart, Quint, Strong, Elastic, Back, SteppedEase, Bounce, Sine, Expo, Circ, TweenLite, TimelineLite, TimelineMax } from "./gsap-core.js";
import { CSSPlugin } from "./CSSPlugin.js";
var gsapWithCSS = gsap.registerPlugin(CSSPlugin) || gsap, // <--- This line here.
    // to protect from tree shaking 
TweenMaxWithCSS = gsapWithCSS.core.Tween;
export { gsapWithCSS as gsap, gsapWithCSS as default, CSSPlugin, TweenMaxWithCSS as TweenMax, TweenLite, TimelineMax, TimelineLite, Power0, Power1, Power2, Power3, Power4, Linear, Quad, Cubic, Quart, Quint, Strong, Elastic, Back, SteppedEase, Bounce, Sine, Expo, Circ };

Is there a way to tweak tree shaking even in this case and avoid including CSSPlugin and gsap-core at all if the client code (in my case pigretto) using js-utl doesn't use the functions exported by js-utl which require gsap like transform and/or fadeIn?

Thank you for the attention and for this library.

jackdoyle commented 4 years ago

Hm, well from your sample code above it looks like you are animating things that'd require CSSPlugin (like transforms and opacity...I assume that's CSS-related, right?)

But advanced users could get around the CSSPlugin auto-activation by only importing directly from gsap-core instead of the typical "gsap". Of course that means that any CSS-related animations won't work.

Does that answer your question?

tonix-tuft commented 4 years ago

Thank you for your reply @jackdoyle!

Hm, well from your sample code above it looks like you are animating things that'd require CSSPlugin (like transforms and opacity...I assume that's CSS-related, right?)

Yes, that's CSS-related stuff as well as very simple animations. The thing is, all this functionality is not used by pigretto, which is a proxy library, but CSSPlugin and gsap-core ends up being bundled in the final pigretto build (transform(), fadeIn(), fadeOut() don't), because gsap's code prevents it to be tree-shaked.

I tried to do the following:

// I created an extra file called js-utl/src/externals/gsap.js
// with the following content:
import { gsap } from "gsap/gsap-core";
import { CSSPlugin } from "gsap/CSSPlugin";

/**
 * @type {boolean}
 */
let registered = false;

/**
 * @type {null|gsap}
 */
let GSAP = null;

export default function getGSAP() {
  if (!registered) {
    registered = true;
    gsap.registerPlugin(CSSPlugin);
    GSAP = gsap;
  }
  return GSAP;
}

Then, in animation.js:

...

/**
 * Animation utility functions.
 */

import { millisecToSec } from "./time";
import getGSAP from "../externals/gsap";

...
export function fadeIn(node, options) {
  const opt = options || {};
  const secs = millisecToSec(opt.millisec || 300);
  const css = opt.css || {};
  const gsap = getGSAP();
  gsap.fromTo(
    node,
    secs,
    {
      opacity: 0,
      display: css.display || "block",
      ...(opt.fromCSS || {})
    },
    {
      opacity: 1,
      ...(opt.toCSS || {}),
      onComplete: () => {
        opt.onComplete && opt.onComplete();
      }
    }
  );
}

...
export function fadeOut(node, options) {
  const opt = options || {};
  const secs = millisecToSec(opt.millisec || 300);
  const css = opt.css || {};
  const gsap = getGSAP();
  gsap.fromTo(
    node,
    secs,
    {
      opacity: 1,
      display: css.display || "block",
      ...(opt.fromCSS || {})
    },
    {
      opacity: 0,
      ...(opt.toCSS || {}),
      onComplete: () => {
        gsap.set(node, {
          display: css.displayOnComplete || "none"
        });
        opt.onComplete && opt.onComplete();
      }
    }
  );
}

And in css-in-js.js:

...

/**
 * CSS-in-JS utility functions.
 */

import { prefix } from "inline-style-prefixer";
import { cssifyObject, resolveArrayValue } from "css-in-js-utils";
import { isArray } from "./core";
import getGSAP from "../externals/gsap";

...
export function transform(element, ...transforms) {
  const gsap = getGSAP();
  gsap.set(element, Object.assign({}, ...transforms));
}

But even this way, pigretto using js-utl ends up with CSSPlugin as well as gsap/gsap-core being bundled even though it doesn't use any of the animation.js and css-in-js.js functions exported by js-utl (which in turn import gsap).

But advanced users could get around the CSSPlugin auto-activation by only importing directly from gsap-core instead of the typical "gsap". Of course that means that any CSS-related animations won't work.

If I do not import from gsap/CSSPlugin, but import from gsap/gsap-core only, will import { gsap } from "gsap/gsap-core" include tree-shakable code?

Could you make a snippet of an example where you import gsap/CSSPlugin as well as gsap/gsap-core so that both are tree-shakeable (if it's possible, of course)?

Meanwhile, the alternative solution for me was to split animation.js and css-in-js.js into a new dedicated NPM package requiring gsap as a dependency called gospel, and release a major 4.0.0 version of js-utl without those files using gsap. This way I was able to cut pigretto's final minified bundle size by a factor of ~2 (from ~113 KiB down to ~48.9 KiB). The new gospel package has a minified build of about ~60 KiB and bundles gsap's core and its CSSPlugin.

What is the size of gsap's core together with CSSPlugin when minified? About ~50 KiB, I guess.

Thank you!

OSUblake commented 4 years ago

Could you make a snippet of an example where you import gsap/CSSPlugin as well as gsap/gsap-core so that both are tree-shakeable (if it's possible, of course)?

If you don't use an import, it will be dropped. You are using both in this snippet.

// I created an extra file called js-utl/src/externals/gsap.js
// with the following content:
import { gsap } from "gsap/gsap-core";
import { CSSPlugin } from "gsap/CSSPlugin";

/**
 * @type {boolean}
 */
let registered = false;

/**
 * @type {null|gsap}
 */
let GSAP = null;

export default function getGSAP() {
  if (!registered) {
    registered = true;
    gsap.registerPlugin(CSSPlugin);
    GSAP = gsap;
  }
  return GSAP;
}

Imports aren't conditional. If you need a conditional import, then you have to use dynamic imports. Something like this.

import('/node_modules/gsap/index.js')
  .then((module) => {
    // Do something with the module.
  });
tonix-tuft commented 4 years ago

@OSUblake Yes, I am using both, but this snippet was a file called js-utl/src/externals/gsap.js and its getGSAP() function was only imported and used within js-utl/src/modules/animation.js and js-utl/src/modules/css-in-js.js.

On the other hand, pigretto doesn't use getGSAP() and any of the fadeIn(), fadeOut() and transform() functions exported by js-utl/src/modules/animation.js or js-utl/src/modules/css-in-js.js.

Indeed, those functions of js-utl do not end up in the pigretto's build, but CSSPlugin and gsap-core does (which doesn't make sense), that's why I was looking for a way to tree shake gsap's code away from pigretto's build... as it's not required by pigretto.

OSUblake commented 4 years ago

I don't know how or if tree shaking is so supposed to work for stuff imported in another package. Tree shaking isn't a standard, and it's up to the build tool to decide how it works. You said you were using webpack, but have you tried using rollup?

tonix-tuft commented 4 years ago

Didn't try with rollup, I use Webpack to bundle most of my JS libraries. I use rollup for React libraries though, so maybe I can give it a try in the future!

Thank you, anyway.

I am closing this as I ended up moving all the code requiring gsap in a dedicated package.