2
\$\begingroup\$

I got a problem I'm finding pretty hard to solve. I'm trying to generate calendar months for a datepicker but at only 12 months I see a perf of around ~4-7ms.

This doesn't sound like a problem but it becomes one on lower-tier devices such as smartphones that basically are 1/10th of the speed of a desktop (so double that ms by 10). This also causes somewhat of a overhead which make the perf less then ideal. In the test example generation of 50 months take around 8-10ms (desktop) and 100-150ms (smartphone).

The main problem seem to have to do with the string concatenation and the insertion of the HTML at the end of the code.

Is there any way to do this better and achieve faster performance without simplifying by skipping <td> and using <divs> + ugly CSS instead?

Here is the code that generates all the months:

// helpers
function _id(e) { return document.getElementById(e); }
function _for(e,f) { var i, len=e.length; for(i=0;i<len;i++){ if(f(e[i]) === false) break; }}
function _data(e,attr) { return e.getAttribute('data-' + attr); }


var monthsNum = 50, // Number of months to generate
    totalNum = monthsNum;
var month = 1, // zero-based index
	year = 2016,
	out = '',
	monthArr = 'January,February,March,April,May,June,Juli,August,September,October,November,December'.split(','),
	dayArr = 'Mon,Tue,Wed,Thu,Fri,Sat,Sun'.split(',');


 // Begin perf measure
var perfStart = performance.now();
	
while(monthsNum--){
	
	month++; // add 1 month for each iteration
	
	// apply new year if month becomes 13, (jan = 1, dec = 12 etc)
	if(month === 13) { year++; month = 1; }
	
	days = [];
	
	// set days 1
	var date = new Date(year, month, 0),
		totalDays = date.getDate(),
		endDay = date.getDay();
	
	// set days 2
	date.setDate(0);
	var startDay = date.getDay(0),
		nextMonthStart = false,
		prevMonthDays = 0;
		
	// check if startdate isn't 0 (startdate of current month starts at first index)
	if( this.startDay !== 0 ) {
		prevMonthDays = (new Date(year,month-1,0)).getDate() - startDay;
	}
	
	var count = 0,
		day;
	
	for (var i = 0; i < 42; i++) {
		
		day = {};
		
		if(i < startDay) {
			// prev months days
			day.date = prevMonthDays = prevMonthDays + 1;
		}
		else if(i > totalDays + (startDay - 1)) {
			// days after current month
			day.date = count = count + 1;
			if (!nextMonthStart) nextMonthStart = i
		}
		else {
			// regular in-month days
			day.date = i - startDay + 1;
		}
		
		days[days.length] = day.date
	
	}; // end loop
	
	// BEGIN OUTPUT
	out += '<div class="r_month"><em class="r_title">' + monthArr[month-1] + ' <span>' + year + '</span></em><table><thead><tr>';
	_for(dayArr, function(e){ out += '<td>' + e + '</td>'; }) // push weekday names
	
	// end thead, begin tbody
	out += '</tr></thead><tbody>';
	
	// All days
	var i = 0;
	for(var key in days){
		i++;

		// START row (~2ms)
		if(i === 1) out += '<tr>';
			
			// not in current month
			if( key < startDay || ( key >= nextMonthStart ) )  {
				out += '<td class="notCurMonth">&nbsp;</td>';
			}
			else {
				date = days[key] + '/' + month + '/' + year; // DD/MM/YYYY
				out += '<td data-date="'+ date +'"><i>'+days[key]+'</i></td>';
			}
	
		// END row
		if(i === 7) { out += '</tr>'; i=0; }
		
	}
	
	// push end
	out += '</tbody></table></div>'; // end .r_table etc
	
} // end while-loop


// output out-array to #dp
r_dp.innerHTML = out;

// log performance
var perfEnd = performance.now();
var log = 'getMonth perf @ '+ totalNum +' months: ' + Math.round((perfEnd-perfStart)*100)/100 + 'ms';
console.log(log);
r_dp.innerHTML = '<h3>' + log + '</h3>' + r_dp.innerHTML;
html { box-sizing: border-box; font-size:62.5%; font-family: arial, helvetica, sans-serif; }
*, *:before, *:after { box-sizing: inherit; }
body { padding:10px 0; background: #fafafa; font-size:1.6rem; color:#333; }
i { font-style:normal; }
h3 { margin: 10px 0; }

#r_dp {
	width: 95%;
	margin:0 auto;
	position:relative;
}
.r_month { width: 280px; float:left; margin-right:20px;  margin-bottom:20px; background: #fff; }
table { width:100%; border-collapse:collapse; }
td {
  width:auto;
  padding: 5px; border:1px solid #ddd; 
  text-align:center;
}

.r_title {
    padding:10px 5px;
    text-align:center;
    font-size:12px;
    display:block;
    font-style:normal;
	width:100%;
	background: #03A9F4;
	color:#fff;
	text-transform:uppercase;
}
<div id="r_dp"></div>

\$\endgroup\$
3
  • \$\begingroup\$ FYI, Code Review supports Stack Snippets; you could have that jsfiddle embedded in your post and executable on-site - simply edit and Ctrl+M =) \$\endgroup\$ – Mathieu Guindon Feb 1 '16 at 15:32
  • \$\begingroup\$ Did you mean to provide a link for that jsFiddle? There wasn't already one. Please wait five minutes from my edit before adding it. \$\endgroup\$ – Jamal Feb 1 '16 at 16:13
  • \$\begingroup\$ There's the internal thingy now instead. Just press "Run code snippet"? \$\endgroup\$ – Tommie Feb 1 '16 at 16:15
1
\$\begingroup\$

Initial solution (~ 15 ms)

Interested by your question, I first looked for a different strategy to make it faster. Then I ended with a much reduced code, which was also more readable... but it ran much lower than yours!

So I went back to the initial version, and followed your thought about skipping some HTML parts. And you're right, it's the right way to make it faster: on my desktop your version took about 20 ms, while the new one tooks only about 15 ms.

To achieve that, I changed what follows:

  • drop class="r-month" from the <div>s and changed the .r-month {} rule into #r-month div{}
  • drop class="r-title" from the <em>s and changed the .r-title {} rule into #r-title em {}
  • drop the </tr> and </td>, which are optional with HTML5

I also dropped tags that seem not required here, but maybe you'll want to keep them for accessibility purposes:

  • <thead> + </thead> and <tbody> + </tbody>
  • <i> + </i>

A better solution (~ 15 ms, increased readability)

After reading your comments under this answer I did some additional tests, so I discovered something really important: the larger part of the time spent to achieve the whole work comes from r_dp.innerHTML = ... affectation.

It happens that my initial alternative strategy used direct implementation in the final HTML code, so using a lot of such affectations.
In other words, while your version schematically looks like:

var out = '';
while (...) {
  ...
  out += ... // n times
  ...
}
r_dp.innerHTML = out;

the mine worked like this:

while (...) {
  ...
  r_dp.innerHTML += ... // n times
  ...
}

So once I realized that those multiple affectations had a great impact on the time spent, I simply turned back to a unique final affectation.
The result is that now my version:

  • has a reduced code, with increased readability, as already evoked
  • and works faster (about 15 ms) without any of the HTML restrictions I suggested above

Here is this version:

var monthsNum = 50, // Number of months to generate
    month = 1, // zero-based index
    year = 2016,
    out = '';

    const MONTHS = [
    'January', 'February', 'March', 'April', 'May', 'June',
    'Juli', 'August', 'September', 'October', 'November', 'December'
  ];
  const WEEK_HEAD = 
    `${['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
    .reduce((row, e) => `${row}<td>${e}</td>`, '')}`;

function _month(begMonth, endMonth, monthYear) {
  var grid = '',
      weekFirstDay = (begMonth.getDay() + 6) % 7,
      monthLastDay = endMonth.getDate();
  for (var i = 0; i < 6; i++) {
    grid += `<tr>${_week(i * 7, weekFirstDay, monthLastDay, monthYear)}</tr>`;
  }
  return grid;
}

function _week(daysOffset, weekFirstDay, monthLastDay, monthYear) {
  var row = '';
  for (var j = daysOffset, n = daysOffset + 6; j <= n; j++) {
    var day = j - weekFirstDay + 1;
    row += `<td ${
      j < weekFirstDay || day > monthLastDay ?
        'class="notCurMonth">&nbsp;'
        : `data-date="${day}/${monthYear}"><i>${day}</i>`
    }</td>`;
  }
  return row;
}

// Begin perf measure
var perfStart = performance.now();

for (var i = 0; i < monthsNum; i++) {
  var begMonth = new Date(year, month + i, 1),
      endMonth = new Date(year, month + i + 1, 0),
      curMonth = begMonth.getMonth(),
      curYear = begMonth.getFullYear();
  out += `
<div class="r_month">
  <em class="r_title">
    ${MONTHS[curMonth]} <span>${curYear}</span>
  </em>
  <table>
    <thead><tr>${WEEK_HEAD}</tr></thead>
    <tbody>${_month(begMonth, endMonth, `${curMonth + 1}/${curYear}`)}</tbody>
  </table>
</div>`;
}

r_dp.innerHTML = out;

// log performance
var perfEnd = performance.now();
var log = 'getMonth perf @ '+ monthsNum +' months: ' + Math.round((perfEnd-perfStart)*100)/100 + 'ms';
console.log(log);
r_dp.innerHTML = '<h3>' + log + '</h3>' + r_dp.innerHTML;
html { box-sizing: border-box; font-size:62.5%; font-family: arial, helvetica, sans-serif; }
*, *:before, *:after { box-sizing: inherit; }
body { padding:10px 0; background: #fafafa; font-size:1.6rem; color:#333; }
i { font-style:normal; }
h3 { margin: 10px 0; }

#r_dp {
	width: 95%;
	margin:0 auto;
	position:relative;
}
.r_month { width: 280px; float:left; margin-right:20px;  margin-bottom:20px; background: #fff; }
table { width:100%; border-collapse:collapse; }
td {
  width:auto;
  padding: 5px; border:1px solid #ddd; 
  text-align:center;
}

.r_title {
    padding:10px 5px;
    text-align:center;
    font-size:12px;
    display:block;
    font-style:normal;
	width:100%;
	background: #03A9F4;
	color:#fff;
	text-transform:uppercase;
}
<body id="r_dp"></body>


Little enhancement (~ 13 ms)

If we also apply some of the HTML restrictions proposed in my previous solution (but not the ones you pointed as inconvenient in your comments), we improve performance yet, about 13 ms.

Here is it:

var monthsNum = 50, // Number of months to generate
    month = 1, // zero-based index
    year = 2016,
    out = '';

    const MONTHS = [
    'January', 'February', 'March', 'April', 'May', 'June',
    'Juli', 'August', 'September', 'October', 'November', 'December'
  ];
  const WEEK_HEAD = 
    `${['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
    .reduce((row, e) => `${row}<td>${e}`, '')}`;

function _month(begMonth, endMonth, monthYear) {
  var grid = '',
      weekFirstDay = (begMonth.getDay() + 6) % 7,
      monthLastDay = endMonth.getDate();
  for (var i = 0; i < 6; i++) {
    grid += `<tr>${_week(i * 7, weekFirstDay, monthLastDay, monthYear)}`;
  }
  return grid;
}

function _week(daysOffset, weekFirstDay, monthLastDay, monthYear) {
  var row = '';
  for (var j = daysOffset, n = daysOffset + 6; j <= n; j++) {
    var day = j - weekFirstDay + 1;
    row += `<td ${
      j < weekFirstDay || day > monthLastDay ?
        'class="notCurMonth">&nbsp;'
        : `data-date="${day}/${monthYear}"><i>${day}</i>`
    }`;
  }
  return row;
}

// Begin perf measure
var perfStart = performance.now();

for (var i = 0; i < monthsNum; i++) {
  var begMonth = new Date(year, month + i, 1),
      endMonth = new Date(year, month + i + 1, 0),
      curMonth = begMonth.getMonth(),
      curYear = begMonth.getFullYear();
  out += `
<div>
  <em>
    ${MONTHS[curMonth]} ${curYear}
  </em>
  <table>
    <thead><tr>${WEEK_HEAD}</thead>
    <tbody>${_month(begMonth, endMonth, `${curMonth + 1}/${curYear}`)}</tbody>
  </table>
</div>`;
}

r_dp.innerHTML = out;

// log performance
var perfEnd = performance.now();
var log = 'getMonth perf @ '+ monthsNum +' months: ' + Math.round((perfEnd-perfStart)*100)/100 + 'ms';
console.log(log);
r_dp.innerHTML = '<h3>' + log + '</h3>' + r_dp.innerHTML;
html {
  box-sizing: border-box;
  font-size:62.5%;
  font-family: arial,
  helvetica, sans-serif;
}
*, *:before, *:after {
  box-sizing: inherit;
}
body {
  padding:10px 0;
  background: #fafafa;
  font-size:1.6rem;
  color:#333;
}
i {
  font-style:normal;
}
h3 {
  margin: 10px 0;
}
#r_dp {
	width: 95%;
	margin:0 auto;
	position:relative;
}
#r_dp div {
  width: 280px;
  float:left;
  margin-right:20px;
  margin-bottom:20px;
  background: #fff;
}
table {
  width:100%;
  border-collapse:collapse;
}
td {
  width:auto;
  padding: 5px;
  border:1px solid #ddd; 
  text-align:center;
}
#r_dp em {
  padding:10px 5px;
  text-align:center;
  font-size:12px;
  display:block;
  font-style:normal;
	width:100%;
	background: #03A9F4;
	color:#fff;
	text-transform:uppercase;
}
<body id="r_dp"></body>


More enhancement (~ 12 ms, more increased readability)

Going ahead from this, I had an idea for a possible yet more improved version, where a whole HTML skeleton is previously buit, then used for each month.
This way, it's a little faster and code is yet more reduced and readable.

Here is it:

var monthsNum = 50, // Number of months to generate
    month = 1, // zero-based index
    year = 2016,
    out = '';

const
  MONTHS = [
    'January', 'February', 'March', 'April', 'May', 'June',
    'Juli', 'August', 'September', 'October', 'November', 'December'
  ],
  WEEK_HEAD = 
    `${['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
    .reduce((row, e) => `${row}<td>${e}`, '')}`,
  GRID_SKELETON = `
<div>
  <em>@title</em>
  <table>
    <thead><tr>@head</thead>
    <tbody>${`<tr>${'<td @'.repeat(7)}`.repeat(6)}</tbody>
  </table>
</div>`,
  EMPTY_CELL = 'class="notCurMonth">&nbsp;';

// Begin perf measure
var perfStart = performance.now();

for (var i = 0; i < monthsNum; i++) {
  var begMonthDate = new Date(year, month + i, 1),
      curMonth = begMonthDate.getMonth(),
      curYear = begMonthDate.getFullYear(),
      weekFirstDay = (begMonthDate.getDay() + 6) % 7,
      endMonthDay = new Date(year, month + i + 1, 0).getDate(),
      cell = 0,
      day = 0;
  out += GRID_SKELETON
    .replace(/@title/, `${MONTHS[curMonth]} ${curYear}`)
    .replace(/@head/, WEEK_HEAD)
    .replace(/(@)/g, () => (
      (cell++ < weekFirstDay || day >= endMonthDay) ? EMPTY_CELL
      : `data-date="${++day}/${curMonth + 1}/${curYear}">${day}`
    ))
}

// output out-array to #dp
r_dp.innerHTML = out;

// log performance
var perfEnd = performance.now();
var log = 'getMonth perf @ '+ monthsNum +' months: ' + Math.round((perfEnd-perfStart)*100)/100 + 'ms';
console.log(log);
r_dp.innerHTML = '<h3>' + log + '</h3>' + r_dp.innerHTML;
html {
  box-sizing: border-box;
  font-size:62.5%;
  font-family: arial,
  helvetica, sans-serif;
}
*, *:before, *:after {
  box-sizing: inherit;
}
body {
  padding:10px 0;
  background: #fafafa;
  font-size:1.6rem;
  color:#333;
}
i {
  font-style:normal;
}
h3 {
  margin: 10px 0;
}
#r_dp {
	width: 95%;
	margin:0 auto;
	position:relative;
}
#r_dp div {
  width: 280px;
  float:left;
  margin-right:20px;
  margin-bottom:20px;
  background: #fff;
}
table {
  width:100%;
  border-collapse:collapse;
}
td {
  width:auto;
  padding: 5px;
  border:1px solid #ddd; 
  text-align:center;
}
#r_dp em {
  padding:10px 5px;
  text-align:center;
  font-size:12px;
  display:block;
  font-style:normal;
	width:100%;
	background: #03A9F4;
	color:#fff;
	text-transform:uppercase;
}
<body id="r_dp"></body>

\$\endgroup\$
6
  • \$\begingroup\$ Thank you for trying! I understand that you find this interesting as i do too. :) The dropping of tags is a sure way to increase performance but that is only if the goal is the performance and not the function. The i-tags are needed to separate things later on, this month-generator is part of a datepicker etc. One can also drop using a table and go for <div><i><u><q> tags but then one face all sort of cross browser issues later and instead will get messy CSS and it just isn't worth it. \$\endgroup\$ – Tommie Feb 4 '16 at 12:42
  • \$\begingroup\$ Continued... I have re-define since i came to think about performance vs percieved performance. The thing is -- i will at any given time show at most 2 months anyway alas i can generate the two first iterations, add a setTimeout with 0 duration. What this do is generate the first two months in around 0.2-0.4ms and then generate the 48 that's left after that. For the user the effect is: 2 months quickly shows up, after ~50ms the rest of the months show..... \$\endgroup\$ – Tommie Feb 4 '16 at 12:43
  • \$\begingroup\$ ... This is a non-problem since the reaction time of the user is way above 50ms (lower for more realistic numbers like 24 months) and since the user will never see more then 2 months anyway or have time to react and wonder why it doesn't work it's sort of solved. What i'm working on now is moving things to its own functions so that it gets more simple to separate the two different loops (one for month 1+2 and one for the rest). \$\endgroup\$ – Tommie Feb 4 '16 at 12:45
  • \$\begingroup\$ ... I understand how this isn't understood since the initial post doesn't say anything about a datepicker. But if one think of a field that the user bring focus > a datepicker is displayed it becomes more clear. One could also think that it would be simpler just to generate 2x months per < prev next > navigation but that causes a plethora more complexity in the code (too long to get into) and i'm creating a date range picker specifically for the travel industry where one goal is that it should be under 5kb (un-gzipped), responsive etc. \$\endgroup\$ – Tommie Feb 4 '16 at 12:50
  • \$\begingroup\$ @Tommie I have some trouble to clearly understand all the implications of what you mentioned here, and will try better further. But for now, I already have a better solution than the previous one: look at my edit. \$\endgroup\$ – cFreed Feb 4 '16 at 19:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.