14 functions so you can dump lodash and reduce your bundle size…

Lodash and underscore changed the way I write Javascript forever, but today there might be better options for the most common functions.

I recently went through our main app looking to reduce the bundle size and quickly identified that we were still g…


This content originally appeared on DEV Community and was authored by Mike Talbot

Lodash and underscore changed the way I write Javascript forever, but today there might be better options for the most common functions.

I recently went through our main app looking to reduce the bundle size and quickly identified that we were still getting most of lodash imported despite our best efforts to do specific functional imports.

We moved to lodash-es and that helped a bit, but I was still looking at a couple of utility functions taking up around 30% of the bundle.

The problem is that, as a node module, many of the choices about polyfilling old functionality have already been made by the library, so depending on your target browser you might have a lot of code you don't need.

I identified 14 core functions we used from lodash and went about re-writing them in modern Javascript so the bundling process can decide what it needs to provide in terms of polyfills depending on the target. The reductions in import size were significant.

The core functions

Here's what I did about that list of functions:

Matched functionality

  • filter
  • forEach (arrays and objects)
  • groupBy
  • keyBy
  • map (arrays and objects)
  • merge
  • omit
  • sortBy
  • uniq
  • uniqBy

Implemented "enough"

  • pick
  • get (doesn't support array syntax)
  • set (doesn't support array syntax)
  • debounce (with maxWait, flush, cancel)

The functions

So here are those functions, what they do and how I implemented them:

pick(function(item)=>value | propertyName)

We will start with pick because it's pretty useful for everything else. pick will return a function to extract a property from an object - my implementation will convert a string to this, but leave other values alone.

You can use pick yourself like this:

const array = [{ name: "mike", a: 1 }, { name: "bob", a: 2 }]

console.log(array.map(pick('name')) //=> ["mike", "bob"]

Implementation

export function pick(fn) {
  return typeof fn === "string" ? (v) => v[fn] : fn
}

filter(array, function(item)=>boolean | string)

We used filter with a name property quite a lot, so filter is basically just pick and the existing filter function:

const array = [{ name: "mike", a: 1 }, { name: "bob", a: 2 }, { a: 4 }]

console.log(filter(array, 'name')) //=> [{ name: "mike", a: 1 }, { name: "bob", a: 2 }]

Implementation

import { pick } from "./pick"

export function filter(target, fn) {
  return target.filter(pick(fn))
}

forEach(array|object, function(value, key))

In lodash we can use either an object or an array for a forEach and so we needed an implementation that can do that. The callback gets the parameters value and key. It works like this:

const data = { a: 1, b: 2, d: "hello" }
forEach(data, (value, key)=>console.log(`${key}=${value}`) 
      //=> a=1
      //=> b=2
      //=> d=hello

Implementation

import { pick } from "./pick"

export function applyArrayFn(target, fnName, fn) {
  fn = pick(fn)
  if (Array.isArray(target)) return target[fnName](fn)
  if (target && typeof target === "object")
    return Object.entries(target)[fnName](([key, value]) => fn(value, key))
  throw new Error(`Cannot iterate ${typeof target}`)
}

export function forEach(target, fn) {
  return applyArrayFn(target, "forEach", fn)
}

get(object, propertyPath, defaultValue)

get allows you to read properties from an object and if any intermediaries or the final value are not found it will return the default value

const data = { a: { b: {d: 1 } } }
get(data, "a.b.d") //=> 1
get(data, "a.c.d", "hmmm") //=> hmmm

Implementation

export function get(object, path, defaultValue) {
  const parts = path.split(".")
  for (let part of parts) {
    object = object[part]
    if (!object) return defaultValue
  }
  return object
}

groupBy(array, function(item)=>key | propertyName)

Create an object keyed by the result of a function (or picked property name) where every value is an array of the items which had the same key.

const array = [{ name: "mike", type: "user" }, { name: "bob", type: "user" }, { name: "beth", type: "admin"} ]

console.log(groupBy(array, 'type'))
    /*=>
       {
          admin: [{name: "beth", type: "admin" }],
          user: [{name: "mike", type: "user" }, {name: "bob", type: "user"}]
       }
    */

Implementation

import { pick } from "./pick"

export function groupBy(target, fn) {
  fn = pick(fn)
  return target
    .map((value) => ({ value, key: fn(value) }))
    .reduce((c, a) => {
      c[a.key] = c[a.key] || []
      c[a.key].push(a.value)
      return c
    }, {})
}

keyBy(array, function(item)=>key | propertyName)

Similar to groupBy but the result is the last item which matched a key - usually this is given something where the key will be unique (like an id) to create a lookup

const array = [{ id: "a7", name: "mike", type: "user" }, { id: "z1", name: "bob", type: "user" }, { id: "a3", name: "beth", type: "admin"} ]

console.log(keyBy(array, 'id'))
    /*=>
       {
          "a3": {name: "beth", type: "admin", id: "a3" },
          "a7": {name: "mike", type: "user", id: "a7" },
          "z1": {name: "bob", type: "user", id: "z1"}
       }
    */

Implementation

import { pick } from "./pick"

export function keyBy(target, fn) {
  fn = pick(fn)
  return target
    .map((value) => ({ value, key: fn(value) }))
    .reduce((c, a) => {
      c[a.key] = a.value
      return c
    }, {})
}

map(array|object, function(value, key)=>value | propertyName)

Maps both objects and arrays (like forEach)

const records = {
          "a3": {name: "beth", type: "admin" },
          "a7": {name: "mike", type: "user" },
          "z1": {name: "bob", type: "user"}
       }
console.log(map(records, 'name')) /=> ["beth", "mike", "bob"]

Implementation

import { pick } from "./pick"

export function applyArrayFn(target, fnName, fn) {
  fn = pick(fn)
  if (Array.isArray(target)) return target[fnName](fn)
  if (target && typeof target === "object")
    return Object.entries(target)[fnName](([key, value]) => fn(value, key))
  throw new Error(`Cannot iterate ${typeof target}`)
}

export function forEach(target, fn) {
  return applyArrayFn(target, "map", fn)
}

merge(target, ...sources)

Works like Object.assign but recurses deep into the underlying structure to update the deeper objects rather than replacing them.

const record = { id: "2", name: "Beth", value: 3, ar: ["test", { a: 3, d: { e: 4 } }] }
console.log(merge(record, { ar: [{ b: 1 }, { c: 3, d: { f: 5 } }]))

   /*=>
    {
      id: "2",
      name: "Beth",
      value: 3,
      ar: [{ b: 1 }, { c: 3, d: { f: 5, e: 4 } }]
    }
   */

Implementation

export function merge(target, ...sources) {
  for (let source of sources) {
    mergeValue(target, source)
  }

  return target

  function innerMerge(target, source) {
    for (let [key, value] of Object.entries(source)) {
      target[key] = mergeValue(target[key], value)
    }
  }

  function mergeValue(targetValue, value) {
    if (Array.isArray(value)) {
      if (!Array.isArray(targetValue)) {
        return [...value]
      } else {
        for (let i = 0, l = value.length; i < l; i++) {
          targetValue[i] = mergeValue(targetValue[i], value[i])
        }
        return targetValue
      }
    } else if (typeof value === "object") {
      if (targetValue && typeof targetValue === "object") {
        innerMerge(targetValue, value)
        return targetValue
      } else {
        return value ? { ...value } : value
      }
    } else {
      return value ?? targetValue ?? undefined
    }
  }
}

omit(object, arrayOfProps)

Returns an object with the props listed removed

const record = { a: 1, b: 2, c: 3}
console.log(omit(record, ['b', 'c'])) //=> {a: 1}

Implementation

export function omit(target, props) {
  return Object.fromEntries(
    Object.entries(target).filter(([key]) => !props.includes(key))
  )
}

set(object, propertyPath, value)

Sets a value on an object, creating empty objects {} along the way if necessary.

const record = { a: 1, d: { e: 1 } }
set(record, "a.d.e", 2) //=> { a: 1, d: { e: 2 } }
set(record, "a.b.c", 4) //=> { a: 1, b: { c: 4 }, d: { e: 2 } }

Implementation

export function set(object, path, value) {
  const parts = path.split(".")
  for (let i = 0, l = parts.length - 1; i < l; i++) {
    const part = parts[i]
    object = object[part] = object[part] || {}
  }
  object[parts[parts.length - 1]] = value
}

sortBy(array, function(item)=>value | propertyName)

Sort an array by a sub element.

const array = [{ id: "a7", name: "mike", type: "user" }, { id: "z1", name: "bob", type: "user" }, { id: "a3", name: "beth", type: "admin"} ]
console.log(sortBy(array, 'name'))
     /*=>
      [
        { id: "a3", name: "beth", type: "admin"} 
        { id: "z1", name: "bob", type: "user" }, 
        { id: "a7", name: "mike", type: "user" }, 
      ]
     */

Implementation

import { pick } from "./pick"

export function sortBy(array, fn) {
  fn = pick(fn)
  return array.sort((a, b) => {
    const va = fn(a)
    const vb = fn(b)
    if (va < vb) return -1
    if (va > vb) return 1
    return 0
  })
}

uniq(array)

Make a unique array from an existing array

const array = ['a', 'b', 'c', 'b', 'b', 'a']
console.log(uniq(array)) //=> ['a', 'b', 'c']

Implementation

export function uniq(target) {
  return Array.from(new Set(target))
}

uniqBy(array, function(item)=>value | propertyName)

Make a uniq array using a property of objects in the array.

const array = [{a: 1, b: 2}, {a: 4, b: 2}, {a: 5, b: 3}]
console.log(uniqBy(array, 'b')) //=> [{a: 1, b: 2}, {a: 5, b: 3}]

Implementation

import { pick } from "./pick"

export function uniqBy(target, fn) {
  fn = pick(fn)
  const dedupe = new Set()
  return target.filter((v) => {
    const k = fn(v)
    if (dedupe.has(k)) return false
    dedupe.add(k)
    return true
  })
}

Partially Implemented debounce

lodash debounce is very powerful - too powerful for me and too big. I just need a function I can debounce, a maximum time to wait and the ability to flush any pending calls or cancel them. (So what is missing is trailing and leading edges etc, + other options I don't use).

const debounced = debounce(()=>save(), 1000, {maxWait: 10000})
...
debounced() // Call the debounced function after 1s (max 10s)
debounced.flush() // call any pending 
debounced.cancel() // cancel any pending calls

Implementation

export function debounce(fn, wait = 0, { maxWait = Infinity } = {}) {
  let timer = 0
  let startTime = 0
  let running = false
  let pendingParams
  let result = function (...params) {
    pendingParams = params
    if (running && Date.now() - startTime > maxWait) {
      execute()
    } else {
      if (!running) {
        startTime = Date.now()
      }
      running = true
    }

    clearTimeout(timer)
    timer = setTimeout(execute, Math.min(maxWait - startTime, wait))

    function execute() {
      running = false
      fn(...params)
    }
  }
  result.flush = function () {
    if (running) {
      running = false
      clearTimeout(timer)
      fn(...pendingParams)
    }
  }
  result.cancel = function () {
    running = false
    clearTimeout(timer)
  }
  return result
}

Conclusion

It's possible to drop the need for lodash if you only use these functions. In our app we do use other lodash functions, but they are all behind lazy imports (so template for instance) - our app is way faster to load as a result.

Feel free to use any of the code in your own projects.


This content originally appeared on DEV Community and was authored by Mike Talbot


Print Share Comment Cite Upload Translate Updates
APA

Mike Talbot | Sciencx (2021-08-16T06:57:29+00:00) 14 functions so you can dump lodash and reduce your bundle size…. Retrieved from https://www.scien.cx/2021/08/16/14-functions-so-you-can-dump-lodash-and-reduce-your-bundle-size/

MLA
" » 14 functions so you can dump lodash and reduce your bundle size…." Mike Talbot | Sciencx - Monday August 16, 2021, https://www.scien.cx/2021/08/16/14-functions-so-you-can-dump-lodash-and-reduce-your-bundle-size/
HARVARD
Mike Talbot | Sciencx Monday August 16, 2021 » 14 functions so you can dump lodash and reduce your bundle size…., viewed ,<https://www.scien.cx/2021/08/16/14-functions-so-you-can-dump-lodash-and-reduce-your-bundle-size/>
VANCOUVER
Mike Talbot | Sciencx - » 14 functions so you can dump lodash and reduce your bundle size…. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2021/08/16/14-functions-so-you-can-dump-lodash-and-reduce-your-bundle-size/
CHICAGO
" » 14 functions so you can dump lodash and reduce your bundle size…." Mike Talbot | Sciencx - Accessed . https://www.scien.cx/2021/08/16/14-functions-so-you-can-dump-lodash-and-reduce-your-bundle-size/
IEEE
" » 14 functions so you can dump lodash and reduce your bundle size…." Mike Talbot | Sciencx [Online]. Available: https://www.scien.cx/2021/08/16/14-functions-so-you-can-dump-lodash-and-reduce-your-bundle-size/. [Accessed: ]
rf:citation
» 14 functions so you can dump lodash and reduce your bundle size… | Mike Talbot | Sciencx | https://www.scien.cx/2021/08/16/14-functions-so-you-can-dump-lodash-and-reduce-your-bundle-size/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.