hebcal / hebcal-es6

perpetual Jewish Calendar with holidays, Shabbat and holiday candle lighting and havdalah times, Torah readings, and more
https://hebcal.github.io/api/core/
GNU General Public License v2.0
102 stars 16 forks source link

Refactor to work with Tree Shaking #475

Open SLaks opened 1 month ago

SLaks commented 1 month ago

Motivation

I wrote some very simple code to run in a browser and print a full year of פרשה names:

import { HebrewCalendar } from '@hebcal/core'
const events = HebrewCalendar.calendar({
  year: new Date().getFullYear() - 1,
  numYears: 2,
  sedrot: true,
  noHolidays: true,
})

console.log(
  events.map((event) => ({
    datetime: event.date.greg(),
    label: event.render('he-x-NoNikud'),
  }))
)

When compiled for production using https://vitejs.dev/ with default settings, this produced 150 kB of optimized JS!

dist/index-Cca6IFY6.js                    150.13 kB │ gzip: 53.69 kB

Since all functions are contained in the HebrewCalendar class and the Event hierarchy, Tree Shaking cannot see which APIs are actually used.

Suggestion

It ought to be possible to build a new modular API in which application code only imports features that are actually used. This should allow tree shaking to entirely drop unused features (eg, emojis, zmanim calculations, unused locales, etc).

This would be a new API that requires explicit imports to enable more calculations and methods. The existing API would remain as a wrapper to avoid breaking existing code. Applications that want to benefit from tree shaking would need to migrate to the new API to gain the benefits.

Details

Making code tree-shakable involves a number of changes (these changes can be made independently for incremental improvements, but each incremental change would require clients to refactor further to get more benefit):

Note: All of these suggestions are rough ideas; I have not reviewed the code carefully enough to be sure that each of these make sense and would actually allow non-trivial amounts of code to be tree-shaken

Let me know if you need help understanding, designing, prototyping, or implementing this.

mjradwin commented 1 month ago

If it's really important to reduce bundle size right now before undertaking such an ambitious refactor, we could do a minor refactor and move Sedra class (and related parshiyot array) from @hebcal/core to @hebcal/hdate and your app could be changed to depend only on the latter. Or make a @hebcal/sedra package that depends only on the latter. Either way it could be done in a way that doesn't break compatibility for @hebcal/core clients because we would still roll it up within@hebcal/core

SLaks commented 1 month ago

I use HebrewCalendar.calendar() here, so that won't help. On further thought, though, I could probably drop that and instead loop over all dates and simply filter out those that have no leinings.

If I do that, I could probably fix this much more simply by making getSedra_() public.

Either way (even if do the full split/refactor), I would also need to refactor hebcal/hebcal-leyning to remove all references to HebrewCalendar; see hebcal/hebcal-leyning#467.
In particular, it calls HebrewCalendar.getHolidaysOnDate(). The simplest approach there is to move it to holidays.ts without changing the API at all (and make the original wrap the moved version). I don't know how much code holidays.ts itself depends on, but it doesn't seem to have any large, non-core dependencies (HolidayEvent and staticHolidays are pretty large, but those are always required), so it probably isn't worth doing any further refactors (dropping YomKippurKatanEvent and modern.ts wouldn't save much code)

Tree shaking should then automatically remove the large dependencies (the entire HebrewCalendar class and everything it references), since I would no longer reference it at all.

I don't think this is particularly urgent for me right now, but I did start working on it.

mjradwin commented 4 weeks ago

Looping over all dates is pretty easy. You'll find this idiom in a few places:

const startAbs = HDate.hebrew2abs(year, TISHREI, 1);
const endAbs = HDate.hebrew2abs(year + 1, TISHREI, 1) - 1;
for (let absDt = startAbs; absDt <= endAbs; absDt++) {
  const events = HebrewCalendar.getHolidaysOnDate(absDt);
  if (events) {
    // ...
  }
}
mjradwin commented 3 weeks ago

Taking a look at this, I have a question for you about how to avoid pulling the Zmanim dependency without breaking backwards compatibility

If we wanted to remove that dependency, how would we handle it? As I'm relatively new to TS, I'm totally open to your suggestions! Do we remove startEvent and endEvent properties from HolidayEvent, and create a FastDayEvent subclass that has those properties?

SLaks commented 3 weeks ago

Done in https://github.com/hebcal/hebcal-es6/commit/c31e0076f0f3669258e0cca0f78f391ab0e078f6

Sorry; I already implemented all of this (except refactoring the calendar() API; I'm going to follow your advice and use a loop instead).

I'm now testing it against leyning.

mjradwin commented 3 weeks ago

Oh, sorry, I didn't realize you were working on this today. I apologize that my getHolidaysOnDate refactor probably caused a bit of a merge mess for you

SLaks commented 3 weeks ago

Your refactor is actually a duplicate of https://github.com/hebcal/hebcal-es6/commit/d0be1bb04c73922018685b42b4b9195451b3cf08, except that I didn't touch getHolidaysForYearArray

SLaks commented 3 weeks ago

Current Status

From my POV, this is complete. With https://github.com/hebcal/hebcal-es6/pull/481 and https://github.com/hebcal/hebcal-leyning/pull/473, https://github.com/SLaks/tikkun.io/tree/use-modular works.

The refactor shrinks the Tikkun's index chunk from 258KB to 151KB!

Possible next steps (I probably will not do these):

mjradwin commented 3 weeks ago

Thanks for this update. So glad this is working.

I think I can safely revert https://github.com/hebcal/hebcal-es6/commit/048f73f0708af283951940a7d81a394c6db2ea58 as you're the one who originally asked for a way to append suffixes in https://github.com/hebcal/hebcal-leyning/issues/432 and if that's no longer an issue, we may as well go back to the simpler build process and avoid duplicate .d.ts