简体   繁体   中英

Issues with parallax/translate3d performance on safari & firefox?

For days I've been trying to figure out how get a nice, smooth, hardware accelerated parallax effect working.

I'm using this repository: https://github.com/GianlucaGuarini/parallax

I've tried throttling with underscores, using css3 transitions to smooth things out, nuking the image quality, but no luck, still jank. It's very smooth in Chrome though. Other repositories I've found either have performance issues, don't work on iOS, or require jQuery.

Any similar experiences or tips with debugging?

The Squarespace team has done an awesome job with their Marquee theme. Not sure how they got it so performant.

Here is a link to the code: https://jsfiddle.net/oh3xwgk1/3/

Edit: One last note. I've done testing with safari and chrome dev tools. Both show durations that are a fraction of a millisecond, so I don't think its related to that?

Edit2: By "jank" I mean jitter or a drop of frames.

HTML

<section class="relative height overflow-hidden fill-black">
    <img class="parallax" src="https://placeimg.com/1000/1000/nature" alt=""> 
</section>

JS

    (function (global, factory) {
  if (typeof define === "function" && define.amd) {
    define('Parallax', ['module'], factory);
  } else if (typeof exports !== "undefined") {
    factory(module);
  } else {
    var mod = {
      exports: {}
    };
    factory(mod);
    global.Parallax = mod.exports;
  }
})(this, function (module) {
  'use strict';

  function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
      throw new TypeError("Cannot call a class as a function");
    }
  }

  var _createClass = function () {
    function defineProperties(target, props) {
      for (var i = 0; i < props.length; i++) {
        var descriptor = props[i];
        descriptor.enumerable = descriptor.enumerable || false;
        descriptor.configurable = true;
        if ("value" in descriptor) descriptor.writable = true;
        Object.defineProperty(target, descriptor.key, descriptor);
      }
    }

    return function (Constructor, protoProps, staticProps) {
      if (protoProps) defineProperties(Constructor.prototype, protoProps);
      if (staticProps) defineProperties(Constructor, staticProps);
      return Constructor;
    };
  }();

  function $$(selector, ctx) {
    var els;
    if (typeof selector == 'string') els = (ctx || document).querySelectorAll(selector);else els = selector;
    return Array.prototype.slice.call(els);
  }

  function extend(src) {
    var obj,
        args = arguments;

    for (var i = 1; i < args.length; ++i) {
      if (obj = args[i]) {
        for (var key in obj) {
          src[key] = obj[key];
        }
      }
    }

    return src;
  }

  function isUndefined(val) {
    return typeof val == 'undefined';
  }

  function elementData(el, attr) {
    if (attr) return el.dataset[attr] || el.getAttribute('data-' + attr);else return el.dataset || Array.prototype.slice.call(el.attributes).reduce(function (ret, attribute) {
      if (/data-/.test(attribute.name)) ret[attribute.name] = attribute.value;
      return ret;
    }, {});
  }

  function prefix(obj, prop, value) {
    var prefixes = ['ms', 'o', 'Moz', 'webkit', ''],
        i = prefixes.length;

    while (i--) {
      var prefix = prefixes[i],
          p = prefix ? prefix + prop[0].toUpperCase() + prop.substr(1) : prop.toLowerCase() + prop.substr(1);

      if (p in obj) {
        obj[p] = value;
        return true;
      }
    }

    return false;
  }

  var observable = function observable(el) {
    el = el || {};

    var callbacks = {},
        slice = Array.prototype.slice,
        onEachEvent = function onEachEvent(e, fn) {
      e.replace(/\S+/g, fn);
    },
        defineProperty = function defineProperty(key, value) {
      Object.defineProperty(el, key, {
        value: value,
        enumerable: false,
        writable: false,
        configurable: false
      });
    };

    defineProperty('on', function (events, fn) {
      if (typeof fn != 'function') return el;
      onEachEvent(events, function (name, pos) {
        (callbacks[name] = callbacks[name] || []).push(fn);
        fn.typed = pos > 0;
      });
      return el;
    });
    defineProperty('off', function (events, fn) {
      if (events == '*' && !fn) callbacks = {};else {
        onEachEvent(events, function (name) {
          if (fn) {
            var arr = callbacks[name];

            for (var i = 0, cb; cb = arr && arr[i]; ++i) {
              if (cb == fn) arr.splice(i--, 1);
            }
          } else delete callbacks[name];
        });
      }
      return el;
    });
    defineProperty('one', function (events, fn) {
      function on() {
        el.off(events, on);
        fn.apply(el, arguments);
      }

      return el.on(events, on);
    });
    defineProperty('trigger', function (events) {
      var args = slice.call(arguments, 1),
          fns;
      onEachEvent(events, function (name) {
        fns = slice.call(callbacks[name] || [], 0);

        for (var i = 0, fn; fn = fns[i]; ++i) {
          if (fn.busy) return;
          fn.busy = 1;
          fn.apply(el, fn.typed ? [name].concat(args) : args);

          if (fns[i] !== fn) {
            i--;
          }

          fn.busy = 0;
        }

        if (callbacks['*'] && name != '*') el.trigger.apply(el, ['*', name].concat(args));
      });
      return el;
    });
    return el;
  };

  var rAF = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame || function (cb) {
    setTimeout(cb, 1000 / 60);
  };

  var RESIZE_DELAY = 20;

  var Stage = function () {
    function Stage() {
      _classCallCheck(this, Stage);

      observable(this);
      this.resizeTimer = null;
      this.tick = false;
      this.bind();
    }

    _createClass(Stage, [{
      key: 'bind',
      value: function bind() {
        var _this = this;

        window.addEventListener('scroll', function () {
          return _this.scroll();
        }, true);
        window.addEventListener('mousewheel', function () {
          return _this.scroll();
        }, true);
        window.addEventListener('touchmove', function () {
          return _this.scroll();
        }, true);
        window.addEventListener('resize', function () {
          return _this.resize();
        }, true);
        window.addEventListener('orientationchange', function () {
          return _this.resize();
        }, true);

        window.onload = function () {
          return _this.scroll();
        };

        return this;
      }
    }, {
      key: 'scroll',
      value: function scroll() {
        var _this2 = this;

        if (this.tick) return this;
        this.tick = !this.tick;
        rAF(function () {
          return _this2.update();
        });
        return this;
      }
    }, {
      key: 'update',
      value: function update() {
        this.trigger('scroll', this.scrollTop);
        this.tick = !this.tick;
        return this;
      }
    }, {
      key: 'resize',
      value: function resize() {
        var _this3 = this;

        if (this.resizeTimer) clearTimeout(this.resizeTimer);
        this.resizeTimer = setTimeout(function () {
          return _this3.trigger('resize', _this3.size);
        }, RESIZE_DELAY);
        return this;
      }
    }, {
      key: 'scrollTop',
      get: function get() {
        var top = (window.pageYOffset || document.scrollTop) - (document.clientTop || 0);
        return window.isNaN(top) ? 0 : top;
      }
    }, {
      key: 'height',
      get: function get() {
        return window.innerHeight;
      }
    }, {
      key: 'width',
      get: function get() {
        return window.innerWidth;
      }
    }, {
      key: 'size',
      get: function get() {
        return {
          width: this.width,
          height: this.height
        };
      }
    }]);

    return Stage;
  }();

  var HAS_TRANSLATE_3D = function (div) {
    prefix(div.style, 'transform', 'translate3d(0, 0, 0)');
    return (/translate3d/g.test(div.style.cssText)
    );
  }(document.createElement('div'));

  var Canvas = function () {
    function Canvas(img, opts) {
      _classCallCheck(this, Canvas);

      observable(this);
      this.opts = opts;
      this.img = img;
      this.wrapper = img.parentNode;
      this.isLoaded = false;
    }

    _createClass(Canvas, [{
      key: 'load',
      value: function load() {
        var _this4 = this;

        if (!this.img.width || !this.img.height || !this.img.complete) this.img.onload = function () {
          return _this4.onImageLoaded();
        };else this.onImageLoaded();
        return this;
      }
    }, {
      key: 'onImageLoaded',
      value: function onImageLoaded() {
        this.isLoaded = true;
        this.update();
        this.trigger('loaded', this.img);
        return this;
      }
    }, {
      key: 'update',
      value: function update() {
        var iw = this.img.naturalWidth || this.img.width,
            ih = this.img.naturalHeight || this.img.height,
            ratio = iw / ih,
            size = this.size;

        if (size.width / ratio <= size.height) {
          this.img.height = size.height;
          this.img.width = size.height * ratio;
        } else {
          this.img.width = size.width;
          this.img.height = size.width / ratio;
        }

        this.img.style.top = - ~ ~((this.img.height - size.height) / 2) + 'px';
        this.img.style.left = - ~ ~((this.img.width - size.width) / 2) + 'px';
        return this;
      }
    }, {
      key: 'draw',
      value: function draw(stage) {
        var size = this.size,
            perc = (this.offset.top + size.height * this.opts.center + stage.height / 2 - stage.scrollTop) / stage.height - 1;
        perc *= this.img.height / size.height / 2 * this.opts.intensity;
        if (HAS_TRANSLATE_3D) prefix(this.img.style, 'transform', 'translate3d(0, ' + -perc.toFixed(4) + '%, 0)');else prefix(this.img.style, 'transform', 'translate(0, ' + -perc + '%, 0)');
        return this;
      }
    }, {
      key: 'bounds',
      get: function get() {
        return this.wrapper.getBoundingClientRect();
      }
    }, {
      key: 'offset',
      get: function get() {
        return {
          top: this.wrapper.offsetTop,
          left: this.wrapper.offsetLeft
        };
      }
    }, {
      key: 'size',
      get: function get() {
        var props = this.bounds;
        return {
          height: props.height | 0,
          width: props.width | 0
        };
      }
    }]);

    return Canvas;
  }();

  var stage;

  var Parallax = function () {
    function Parallax(selector) {
      var opts = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];

      _classCallCheck(this, Parallax);

      observable(this);
      this.opts = opts;
      this.selector = selector;
      this.canvases = [];
      this.add(selector);
      if (!stage) stage = new Stage();
      return this;
    }

    _createClass(Parallax, [{
      key: 'init',
      value: function init() {
        if (!this.canvases.length) {
          console.warn('No images were found with the selector "' + this.selector + '"');
        } else {
          this.imagesLoaded = 0;
          this.bind();
        }

        return this;
      }
    }, {
      key: 'bind',
      value: function bind() {
        var _this5 = this;

        this._onResize = function () {
          for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) {
            args[_key] = arguments[_key];
          }

          return _this5.resize.apply(_this5, args);
        };

        this._onScroll = function () {
          for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
            args[_key2] = arguments[_key2];
          }
           return _this5.scroll.apply(_this5, args);
        };

        stage.on('resize', this._onResize);
        stage.on('scroll', this._onScroll);
        this.canvases.forEach(function (canvas) {
          canvas.one('loaded', function () {
            return _this5.onCanvasLoaded(canvas);
          });
          canvas.load();
        });
        return this;
      }
    }, {
      key: 'refresh',
      value: function refresh() {
        this.onResize(stage.size).onScroll(stage.scrollTop);
        return this;
      }
    }, {
      key: 'onCanvasLoaded',
      value: function onCanvasLoaded(canvas) {
        this.trigger('image:loaded', canvas.img, canvas);
        this.imagesLoaded++;
        canvas.draw(stage);
        if (this.imagesLoaded == this.canvases.length) this.trigger('images:loaded');
        return this;
      }
    }, {
      key: 'scroll',
      value: function scroll(scrollTop) {
        var i = this.canvases.length,
            offsetYBounds = this.opts.offsetYBounds,
            stageScrollTop = stage.scrollTop;

        while (i--) {
          var canvas = this.canvases[i],
              canvasHeight = canvas.size.height,
              canvasOffset = canvas.offset,
              canvasScrollDelta = canvasOffset.top + canvasHeight - stageScrollTop;

          if (canvas.isLoaded && canvasScrollDelta + offsetYBounds > 0 && canvasScrollDelta - offsetYBounds < stageScrollTop + stage.height) {
            canvas.draw(stage);
            this.trigger('draw', canvas.img);
          }
        }

        this.trigger('update', stageScrollTop);
        return this;
      }
    }, {
      key: 'add',
      value: function add(els) {
        this.canvases = this.canvases.concat(this.createCanvases($$(els)));
        return this;
      }
    }, {
      key: 'remove',
      value: function remove(els) {
        var _this6 = this;

        $$(els).forEach(function (el) {
          var i = _this6.canvases.length;

          while (i--) {
            if (el == _this6.canvases[i].img) {
              _this6.canvases.splice(i, 1);

              break;
            }
          }
        });
        return this;
      }
    }, {
      key: 'destroy',
      value: function destroy() {
        this.off('*');
        this.canvases = [];
        stage.off('resize', this._onResize).off('scroll', this._onScroll);
        return this;
      }
    }, {
      key: 'resize',
      value: function resize(size) {
        var i = this.canvases.length;

        while (i--) {
          var canvas = this.canvases[i];
          if (!canvas.isLoaded) return;
          canvas.update().draw(stage);
        }

        this.trigger('resize');
        return this;
      }
    }, {
      key: 'createCanvases',
      value: function createCanvases(els) {
        var _this7 = this;

        return els.map(function (el) {
          var data = elementData(el);
          return new Canvas(el, {
            intensity: !isUndefined(data.intensity) ? +data.intensity : _this7.opts.intensity,
            center: !isUndefined(data.center) ? +data.center : _this7.opts.center
          });
        });
      }
    }, {
      key: 'opts',
      set: function set(opts) {
        this._defaults = {
          offsetYBounds: 50,
          intensity: 30,
          center: 0.5
        };
        extend(this._defaults, opts);
      },
      get: function get() {
        return this._defaults;
      }
    }]);

    return Parallax;
  }();

  module.exports = Parallax;
});

var parallax = new Parallax('.parallax', {
  offsetYBounds: 50,
  intensity: 50,
  center: .75
}).init();

A good solution I found for this was to create a fixed position div and placing it behind the main content. Parallax performance is tricky for big images when you're using parallax so your best case scenario is to use CSS's positioning and javascript to cleverly hide & show. At least in my opinion.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM