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

Replace lodash.debounce with a much smaller, custom debounce function #6550

Merged
merged 2 commits into from
Jan 12, 2025

Conversation

absidue
Copy link
Member

@absidue absidue commented Jan 11, 2025

Replace lodash.debounce with a much smaller, custom debounce function

Pull Request Type

  • Refactor

Description

Lodash's debounce function has served us well but it is quite old so contains a lot of polyfills, also has various options that we don't use in FreeTube and uses the CommonJS module syntax so it doesn't get fully optimised in our builds. This pull request replaces it with a custom, simpler debounce function saving us 1577 bytes in the renderer.js build.

The debounce function is based on the one provided here: https://you-dont-need.github.io/You-Dont-Need-Lodash-Underscore/#/?id=_debounce

For comparision here is the JavaScript of lodash.debounce copied from the node_modules folder with the comments removed so that this isn't too long (it is still rather large, compared to the 17 line (with comments) function that we have now):

Click to expand
var FUNC_ERROR_TEXT = 'Expected a function';

var NAN = 0 / 0;

var symbolTag = '[object Symbol]';

var reTrim = /^\s+|\s+$/g;

var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;

var reIsBinary = /^0b[01]+$/i;

var reIsOctal = /^0o[0-7]+$/i;

var freeParseInt = parseInt;

var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;

var freeSelf = typeof self == 'object' && self && self.Object === Object && self;

var root = freeGlobal || freeSelf || Function('return this')();

var objectProto = Object.prototype;

var objectToString = objectProto.toString;

var nativeMax = Math.max,
  nativeMin = Math.min;

var now = function () {
  return root.Date.now();
};

function debounce(func, wait, options) {
  var lastArgs,
    lastThis,
    maxWait,
    result,
    timerId,
    lastCallTime,
    lastInvokeTime = 0,
    leading = false,
    maxing = false,
    trailing = true;

  if (typeof func != 'function') {
    throw new TypeError(FUNC_ERROR_TEXT);
  }

  wait = toNumber(wait) || 0;

  if (isObject(options)) {
    leading = !!options.leading;
    maxing = 'maxWait' in options;
    maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
    trailing = 'trailing' in options ? !!options.trailing : trailing;
  }

  function invokeFunc(time) {
    var args = lastArgs,
      thisArg = lastThis;
    lastArgs = lastThis = undefined;
    lastInvokeTime = time;
    result = func.apply(thisArg, args);
    return result;
  }

  function leadingEdge(time) {
    lastInvokeTime = time;

    timerId = setTimeout(timerExpired, wait);

    return leading ? invokeFunc(time) : result;
  }

  function remainingWait(time) {
    var timeSinceLastCall = time - lastCallTime,
      timeSinceLastInvoke = time - lastInvokeTime,
      result = wait - timeSinceLastCall;
    return maxing ? nativeMin(result, maxWait - timeSinceLastInvoke) : result;
  }

  function shouldInvoke(time) {
    var timeSinceLastCall = time - lastCallTime,
      timeSinceLastInvoke = time - lastInvokeTime;

    return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
      (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
  }

  function timerExpired() {
    var time = now();
    if (shouldInvoke(time)) {
      return trailingEdge(time);
    }

    timerId = setTimeout(timerExpired, remainingWait(time));
  }

  function trailingEdge(time) {
    timerId = undefined;

    if (trailing && lastArgs) {
      return invokeFunc(time);
    }
    lastArgs = lastThis = undefined;
    return result;
  }

  function cancel() {
    if (timerId !== undefined) {
      clearTimeout(timerId);
    }
    lastInvokeTime = 0;
    lastArgs = lastCallTime = lastThis = timerId = undefined;
  }

  function flush() {
    return timerId === undefined ? result : trailingEdge(now());
  }

  function debounced() {
    var time = now(),
      isInvoking = shouldInvoke(time);
    lastArgs = arguments;
    lastThis = this;
    lastCallTime = time;

    if (isInvoking) {
      if (timerId === undefined) {
        return leadingEdge(lastCallTime);
      }
      if (maxing) {
        timerId = setTimeout(timerExpired, wait);
        return invokeFunc(lastCallTime);
      }
    }
    if (timerId === undefined) {
      timerId = setTimeout(timerExpired, wait);
    }
    return result;
  }
  debounced.cancel = cancel;
  debounced.flush = flush;
  return debounced;
}

function isObject(value) {
  var type = typeof value;
  return !!value && (type == 'object' || type == 'function');
}

function isObjectLike(value) {
  return !!value && typeof value == 'object';
}

function isSymbol(value) {
  return typeof value == 'symbol' ||
    (isObjectLike(value) && objectToString.call(value) == symbolTag);
}

function toNumber(value) {
  if (typeof value == 'number') {
    return value;
  }

  if (isSymbol(value)) {
    return NAN;
  }

  if (isObject(value)) {
    var other = typeof value.valueOf == 'function' ? value.valueOf() : value;
    value = isObject(other) ? (other + '') : other;
  }

  if (typeof value != 'string') {
    return value === 0 ? value : +value;
  }

  value = value.replace(reTrim, '');

  var isBinary = reIsBinary.test(value);

  return (isBinary || reIsOctal.test(value))
    ? freeParseInt(value.slice(2), isBinary ? 2 : 8)
    : (reIsBadHex.test(value) ? NAN : +value);
}
module.exports = debounce;

Testing

  1. Make sure Enable Search Suggestions is turned on in the general settings
  2. Open the Network tab in the devtools
  3. Start typing into the search bar
  4. Check that it only makes a request if you stop typing for a moment and that it uses the most recent query.

(you are basically checking that it does the same as before)

Desktop

  • OS: Windows
  • OS Version: 10
  • FreeTube version: 921a193

@github-actions github-actions bot added PR: dependencies Pull requests that update a dependency file PR: waiting for review For PRs that are complete, tested, and ready for review labels Jan 11, 2025
@FreeTubeBot FreeTubeBot enabled auto-merge (squash) January 11, 2025 15:27
@absidue absidue changed the title Replace lodash.debounce with a much smaller custom debounce function Replace lodash.debounce with a much smaller, custom debounce function Jan 11, 2025
@FreeTubeBot FreeTubeBot merged commit c13c0bf into FreeTubeApp:development Jan 12, 2025
5 checks passed
@github-actions github-actions bot removed the PR: waiting for review For PRs that are complete, tested, and ready for review label Jan 12, 2025
@absidue absidue deleted the smaller-debounce branch January 12, 2025 06:56
SuperAKWA pushed a commit to SuperAKWA/FreeTube that referenced this pull request Jan 24, 2025
…FreeTubeApp#6550)

* Replace lodash.debounce with a much smaller, custom debounce function

* Fix grammar in code comment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
PR: dependencies Pull requests that update a dependency file
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants