eqcss / eqcss

EQCSS is a CSS Reprocessor that introduces Element Queries, Scoped CSS, a Parent selector, and responsive JavaScript to all browsers IE8 and up

Home Page:https://elementqueries.com

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Performance help for bootstrap grid integration

gmournos opened this issue · comments

Hi,
I manage to integrate eqcss with bootstrap-grid.css. However, the performance is terrible and my application is very slow.
Is there an optimisation of EQCSS that would improve performance?
I am already using EQCSS-lite.js but this does not change much.

More specifically:
I am developing a search web application to be embedded in a portal. Typically the users add different sized instances of the application in the same page for different queries they perform, and the application should render according to the div it fits in and not to the screen.
I created my web application using bootstrap-grid.css. Then I changed in this css the media queries with element queries. - changed @media\s*( to @element .myApp and ( - and also prefixed the selectors participating in media queries with :self, so that I could have different instances of my application on the same page.
This worked really nicely and embeeded instances of the application render responsively according to the size of the container the users define in the portal. However, the performance is a killer and each resize takes 5 seconds to calculate.
I am probably killing this.
Any ideas if the integration of EQCSS and bootstrap-grid.css can survive? Any optimizations possible?

Sorry to hear you're having some performance issues here, I'd love to take a peek at what you've got to see if I can come up with performance suggestions, or at least analyze where the slowdown is happening - do you have an isolated test case or can you create an isolated test case where this happens like you're seeing that I could see on Codepen or a fiddle site like that?

Is there an optimisation of EQCSS that would improve performance?

Yes there are some, however I've been working on newer plugins as well and some of the performance of the plugins I'm working on now are 10x the performance you can squeeze out of EQCSS, so depending on what features you're using it may be possible to run the styles with far less code and overhead than EQCSS brings with it by supporting all of the features.

Another question I have is I wonder if there's any way the number of queried elements can be reduced - I'm not sure if you're setting breakpoints on the grid items themselves, or just the container holding them, but one way to improve performance is to put everything in CSS that can be put in CSS, and then try to find ways to consolidate the JS-powered styles. For example if you had a grid of 50 items all of the same size, rather than measuring 50 items you believe are the same width to apply a style, perhaps you could listen to just 1 grid item and apply the style to all 50, etc.

Hi Tom,

I finally decided to make my own lightweight solution.
On window.resize and on some application raised events, I traverse col-xx-x bootstrap classes and inject max-width and flex according to my application's size. This proves very efficient.

I liked the EQCSS ideas and borrowed some of them.

I am posting to you my code. Feel free to comment on it or use it anyway you want, maybe help another eqcss enthousiast in distress :)

(function() {
var MYAPP ='.abcportlet';

var classNames = ['xs', 'sm', 'md', 'lg', 'xl'];
var breakpoints = [0, 480, 768, 992, 1200];
var EQCSS_timeout = 200;

var classes = jQuery(classNames).map(function(i, v) {
	return {min: breakpoints[i], 
		name: classNames[i], 
		max: (i < 4) ? breakpoints[i+1] - 1 : Number.MAX_VALUE
	};
});
//console.log(classes);

var memo;
function memoize(p, idgenerator, calculator) {
	memo = memo || {};
	var pid = idgenerator(p);
	if (memo[pid]) return memo[pid];
	var storeValue = calculator(p);
	//console.log(pid + " calculated new value:" + storeValue);
	memo[pid] = storeValue;
	return memo[pid]; 
}

function memoizeParentWidth(p) {
	return memoize(p, function(p) {return "w-" + p.id}, function(p) {
		return parseInt(window.getComputedStyle(p).getPropertyValue('width'));
	});
}

function memoizeParentRelevantClasses(p) {
	return memoize(p, function(p) {return "rc-" + p.id}, function(p) {
		var parentWidth = memoizeParentWidth(p);
		var relevantClasses = jQuery(classes).filter(function(i, v) {
			return v.max < parentWidth || (v.min < parentWidth && parentWidth < v.max );
		}).map(function(i, v) {return v.name}).toArray();
		return relevantClasses;
	});
}

var getActiveClass = function(e) {
	var relevantClasses = memoizeParentRelevantClasses(jQuery(e).closest(MYAPP)[0]);
	var allColClasses = jQuery(e).attr('class');
	var colClasses = jQuery(allColClasses.split(" ")).filter(function(i, v) {
		return v.startsWith('col');
	}).map(function(i, v) {
		var allClassParts = v.split('-');
		if (allClassParts.length < 2) {
			return {'name': 'xs', 'size': 12, 'index' : 0};
		} else {
			var size = (allClassParts.length === 2) ? allClassParts[1] : allClassParts[2];
			var klaz = (allClassParts.length === 2) ? 'sm' : allClassParts[1];
			return {'name': klaz, 'size': size, 'index' : classNames.indexOf(klaz)};
		}
	}).filter(function(i,v) {
		return relevantClasses.indexOf(v.name) > -1;	
	}).toArray();
	colClasses.sort(function (a, b) {
		return b.index - a.index ;
	});
	//console.log("allColClasses:" + allColClasses);
	//console.log("relevantClasses:" + relevantClasses);
	//console.log("colClasses:" + colClasses);

	if (colClasses.length > 0) {
		return colClasses[0];
	} else 
		return {size : 12};
}

var EQCSS_throttle_available = true;
var EQCSS_throttle_queued = false;


var throttle = function(throttledFunc) {
	if (EQCSS_throttle_available) {
		throttledFunc();
		EQCSS_throttle_available = false;
		setTimeout(function() {
			EQCSS_throttle_available = true;
			if (EQCSS_throttle_queued) {
				EQCSS_throttle_queued = false;
				throttledFunc();
			}
		}, EQCSS_timeout);
	} else {
		EQCSS_throttle_queued = true;
	}
};


var eqcss = {
		run: function() {
			memo = {}; 
			jQuery(MYAPP + ' *[class*="col-"]').each(function(i, e) {
				var activeClass = getActiveClass(e);
				if (activeClass && activeClass.size && activeClass.size !== 'auto') {
					var newWidth = parseInt(activeClass.size) * 100 / 12;
					newWidth = Math.floor(100 * newWidth)/ 100;
					jQuery(e).css('max-width', newWidth + "%");
					jQuery(e).css('flex', "0 0 " + newWidth + "%");
					jQuery(e).css('-ms-flex', "0 0 " + newWidth + "%");
				}
			});	
		},
		updateColWidth: function() { throttle(eqcss.run);}
};

jQuery( document ).ready(eqcss.updateColWidth);
window.addEventListener('resize', eqcss.updateColWidth);

return eqcss;

})();

Thanks! Glad you got something working :D

Here's a sample of where I'm at with trying to refine the ideas in EQCSS, I've got an element() function we can define in JS I can give a selector, a list of element query conditions, and a stylesheet and it will work similar to EQCSS. Then I have another plugin that lets JavaScript read calls to this plugin from CSS stylesheets, so I can express the element queries right in CSS. Similar overall experience to using EQCSS but the performance is much faster!

<div>I'm a div!</div>

<style>
  @supports (--element("div", {"minWidth": 500})) {
    [--self] {
      background: lime;
    }
  }
  @supports (--element("div", {"minWidth": 700})) {
    [--self] {
      background: hotpink;
    }
  }
  @supports (--element("div", {"minWidth": 900})) {
    [--self] {
      background: yellow;
    }
  }
  @supports (--element("div", {"minWidth": 1100})) {
    [--self] {
      background: cyan;
    }
  }
</style>

<script type=module>
  // Our element query plugin (based on jsincss-element-query)
  function element(selector, conditions, stylesheet) {

    // Built-in element query tests
    const features = {
      minWidth: (el, number) => number <= el.offsetWidth,
      maxWidth: (el, number) => number >= el.offsetWidth,
      minHeight: (el, number) => number <= el.offsetHeight,
      maxHeight: (el, number) => number >= el.offsetHeight,
      minChildren: (el, number) => number <= el.children.length,
      children: (el, number) => number === el.children.length,
      maxChildren: (el, number) => number >= el.children.length,
      minCharacters: (el, number) => number <= (el.value && el.value.length)
        || el.textContent.length,
      characters: (el, number) => number === (el.value && el.value.length)
        || el.textContent.length,
      maxCharacters: (el, number) => number >= (el.value && el.value.length)
        || el.textContent.length,
      minScrollX: (el, number) => number <= el.scrollLeft,
      maxScrollX: (el, number) => number >= el.scrollLeft,
      minScrollY: (el, number) => number <= el.scrollTop,
      maxScrollY: (el, number) => number >= el.scrollTop,
      minAspectRatio: (el, number) => number <= el.offsetWidth / el.offsetHeight,
      maxAspectRatio: (el, number) => number >= el.offsetWidth / el.offsetHeight,
      orientation: (el, string) => {
        switch (string) {
          case 'portrait': return el.offsetWidth < el.offsetHeight
          case 'square': return el.offsetWidth === el.offsetHeight
          case 'landscape': return el.offsetWidth > el.offsetHeight
        }
      }
    }

    // Create a custom data attribute we can assign if the tag matches
    const attr = (selector + Object.entries(conditions)).replace(/\W/g, '')

    // For each tag in the document matching the CSS selector
    const result = Array.from(document.querySelectorAll(selector))

    // Process each tag and return a string of CSS that tag needs
    .reduce((output, tag, count) => {

      // Test tag to see if it passes all conditions
      if (
          Object.entries(conditions).every(test =>
            features[test[0]](tag, test[1])
          )
      ) {

        // If the tag passes, set a custom data attribute
        output.add.push({tag: tag, count: count})

        // Add CSS stylesheet to output, replacing [--self] with the current tag
        output.styles.push(
          stylesheet.replace(
            /:self|\$this|\[--self\]/g,
            `${selector}[data-element-atRule-${attr}="${count}"]`
          )
        )

      } else {

        // Otherwise if tag fails tests, remove custom data attribute value
        output.remove.push(tag)

      }

      return output

    }, {styles: [], add: [], remove: []})

    result.add.forEach(
      tag => tag.tag.setAttribute(`data-element-atRule-${attr}`, tag.count)
    )

    result.remove.forEach(
      tag => tag.setAttribute(`data-element-atRule-${attr}`, '')
    )

    return result.styles.join('\n')

  }

  // Deqaf reads js-powered style rules from CSS
  import deqaf from 'https://unpkg.com/deqaf/index.js'

  // Run deqaf with our element query plugin
  deqaf({stylesheet: {element}})
</script>