Merge pull request #936 from sharetribe/landingpage-performance-improvements
Performance improvements
|
|
@ -14,6 +14,10 @@ way to update this template, but currently, we follow a pattern:
|
|||
|
||||
## Upcoming version 2018-09-XX
|
||||
|
||||
* [change] Improve performance of public pages. Image assets are optimized and lazy loading is
|
||||
applied to images in SectionLocation and ListingCard. Read
|
||||
[documentation](./docs/improving-performance.md) for implementation details.
|
||||
[#936](https://github.com/sharetribe/flex-template-web/pull/936)
|
||||
* [change] Update sharetribe-scripts. **cssnext** (used previously in sharetribe-scripts) has been
|
||||
deprecated. Now **postcss-preset-env** is used instead with stage 3 + custom media queries and
|
||||
nesting-rules. If this change breaks your styling, you could still use v1.1.2. The next version of
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ Documentation for specific topics can be found in the following files:
|
|||
* [Original create-react-app documentation](https://github.com/sharetribe/create-react-app/blob/master/packages/react-scripts/template/README.md)
|
||||
* [Customization checklist](customization-checklist.md)
|
||||
* [Icons](icons.md)
|
||||
* [Improving performance](improving-performance.md)
|
||||
|
||||
The application was bootstrapped with a forked version of
|
||||
[create-react-app](https://github.com/facebookincubator/create-react-app). While most of the
|
||||
|
|
|
|||
74
docs/improving-performance.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# Improving page rendering performance
|
||||
|
||||
When we think about page speed there are actually two different scenarios that we need to address:
|
||||
|
||||
* The speed of initial page load and possible reloads after that
|
||||
* The speed of changing the page within Single page application (SPA)
|
||||
|
||||
The first one is usually a slower process. A browser needs to load all the HTML, CSS, JavaScript,
|
||||
and images - and then it needs to understand and execute those files, calculate layout, paint
|
||||
components and finally composite the whole view. The initial page load is the slowest since the
|
||||
consequent page reloads can take benefit from browser caches.
|
||||
|
||||
SPAs can improve from that since they don't necessarily need to download anymore JavaScript, HTML,
|
||||
or CSS - already downloaded JavaScript might be enough for rendering consequent pages when a user
|
||||
wants to navigate to another page. Most of the time SPAs just fetch data for that page.
|
||||
|
||||
These two UX scenarios might also conflict with each other. If all the JavaScript is in one big
|
||||
bundle, page changes within a SPA are fast. However, downloading and evaluating a big JavaScript
|
||||
file is slowing initial page rendering down. Even though users rarely experience the full initial
|
||||
page load speed when they use an SPA like Flex Template for Web, it is good to keep track of that
|
||||
speed. Especially since that is what search engine bots are experiencing and therefore it might
|
||||
affect your page rank.
|
||||
|
||||
Read more about
|
||||
[website performance](https://developers.google.com/web/fundamentals/performance/why-performance-matters/).
|
||||
|
||||
We haven't yet implemented code splitting to reduce initial page rendering time, but there're other
|
||||
improvements that could be done to improve both cases of page rendering.
|
||||
|
||||
## Check page performance
|
||||
|
||||
The first step is, of course, to start measuring performance.
|
||||
[Lighthouse](https://developers.google.com/web/tools/lighthouse/) is a good tool to check rendering
|
||||
performance. At least check those pages that are visible to unauthenticated users (e.g. landing
|
||||
page, search page, listing page, about page and other static pages).
|
||||
|
||||
Lighthouse will give you some tips about how to improve performance and other aspects that website
|
||||
developers should think about.
|
||||
|
||||
## Optimize image sizes
|
||||
|
||||
If your page is showing images, you should check that the image size is not bigger than what is
|
||||
needed. So, adjusting image dimensions is the first step, but you should also think about image
|
||||
quality, advanced rendering options and possibly serving those images from CDN instead of from
|
||||
within your web app.
|
||||
|
||||
Quick checklist:
|
||||
|
||||
* Check that the actual dimensions of an image match with DOM element's dimensions.
|
||||
* Lighthouse suggests that image compression level should be 85% or lower.
|
||||
[Read more](https://developers.google.com/web/tools/lighthouse/audits/optimize-images)
|
||||
* Good rule-of-thumb is that use JPEG for images and photos, where PNG is better for graphics, such
|
||||
as logos, graphs and illustrations.
|
||||
* If you are using JPEG images, think about saving them as progressive JPEGs.
|
||||
[Read more](https://cloudinary.com/blog/progressive_jpegs_and_green_martians) +
|
||||
[Photoshop guide](https://helpx.adobe.com/photoshop-elements/using/optimizing-images-jpeg-format.html)
|
||||
* If you are using PNG images, consider running them through PNG optimizers to reduce file size.
|
||||
Plenty of options available, one example is [TinyPNG.com](https://tinypng.com)
|
||||
* Think about serving images and other static assets from some CDN.
|
||||
[Read more.](https://www.smashingmagazine.com/2017/04/content-delivery-network-optimize-images/)
|
||||
|
||||
## Lazy load off-screen images and other components
|
||||
|
||||
Another way of dealing with images is to lazy load those images that are not visible inside an
|
||||
initially rendered part of the screen. Lazy loading these off-screen images can be done with helper
|
||||
function: `lazyLoadWithDimensions` (from `util/contextHelpers/`). Check `SectionLocations` component
|
||||
for details.
|
||||
|
||||
## Use sparse fields
|
||||
|
||||
Another way to reduce the amount of data that is fetched from API is sparse fields. This is a
|
||||
relatively new feature and Flex template app has not yet leveraged it fully, but it is created to
|
||||
reduce unnecessary data and speed up rendering. You can read more from
|
||||
[Flex API docs](https://flex-docs.sharetribe.com/#sparse-attributes).
|
||||
|
Before Width: | Height: | Size: 846 KiB After Width: | Height: | Size: 160 KiB |
|
|
@ -1,14 +1,15 @@
|
|||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { string, func } from 'prop-types';
|
||||
import { FormattedMessage, intlShape, injectIntl } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { NamedLink, ResponsiveImage } from '../../components';
|
||||
import { lazyLoadWithDimensions } from '../../util/contextHelpers';
|
||||
import { propTypes } from '../../util/types';
|
||||
import { formatMoney } from '../../util/currency';
|
||||
import { ensureListing, ensureUser } from '../../util/data';
|
||||
import { richText } from '../../util/richText';
|
||||
import { createSlug } from '../../util/urlHelpers';
|
||||
import config from '../../config';
|
||||
import { NamedLink, ResponsiveImage } from '../../components';
|
||||
|
||||
import css from './ListingCard.css';
|
||||
|
||||
|
|
@ -33,6 +34,13 @@ const priceData = (price, intl) => {
|
|||
return {};
|
||||
};
|
||||
|
||||
class ListingImage extends Component {
|
||||
render() {
|
||||
return <ResponsiveImage {...this.props} />;
|
||||
}
|
||||
}
|
||||
const LazyImage = lazyLoadWithDimensions(ListingImage, { loadAfterInitialRendering: 3000 });
|
||||
|
||||
export const ListingCardComponent = props => {
|
||||
const { className, rootClassName, intl, listing, renderSizes, setActiveListing } = props;
|
||||
const classes = classNames(rootClassName || css.root, className);
|
||||
|
|
@ -55,7 +63,7 @@ export const ListingCardComponent = props => {
|
|||
onMouseLeave={() => setActiveListing(null)}
|
||||
>
|
||||
<div className={css.aspectWrapper}>
|
||||
<ResponsiveImage
|
||||
<LazyImage
|
||||
rootClassName={css.rootForImage}
|
||||
alt={title}
|
||||
image={firstImage}
|
||||
|
|
|
|||
|
|
@ -16,12 +16,9 @@ exports[`ListingCard matches snapshot 1`] = `
|
|||
onMouseLeave={[Function]}
|
||||
>
|
||||
<div>
|
||||
<ResponsiveImage
|
||||
<lazyLoadWithDimensions(ListingImage)
|
||||
alt="listing1 title"
|
||||
className={null}
|
||||
image={null}
|
||||
noImageMessage={null}
|
||||
rootClassName={null}
|
||||
sizes={null}
|
||||
variants={
|
||||
Array [
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { lazyLoadWithDimensions } from '../../util/contextHelpers';
|
||||
|
||||
import { NamedLink } from '../../components';
|
||||
|
||||
|
|
@ -11,13 +12,21 @@ import helsinkiImage from './images/location_helsinki.jpg';
|
|||
import rovaniemiImage from './images/location_rovaniemi.jpg';
|
||||
import rukaImage from './images/location_ruka.jpg';
|
||||
|
||||
class LocationImage extends Component {
|
||||
render() {
|
||||
const { alt, ...rest } = this.props;
|
||||
return <img alt={alt} {...rest} />;
|
||||
}
|
||||
}
|
||||
const LazyImage = lazyLoadWithDimensions(LocationImage);
|
||||
|
||||
const locationLink = (name, image, searchQuery) => {
|
||||
const nameText = <span className={css.locationName}>{name}</span>;
|
||||
return (
|
||||
<NamedLink name="SearchPage" to={{ search: searchQuery }} className={css.location}>
|
||||
<div className={css.imageWrapper}>
|
||||
<div className={css.aspectWrapper}>
|
||||
<img src={image} alt={name} className={css.locationImage} />
|
||||
<LazyImage src={image} alt={name} className={css.locationImage} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={css.linkText}>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 89 KiB |
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from '../../components';
|
||||
|
||||
import css from './AboutPage.css';
|
||||
import image from './about-us-1440.jpg';
|
||||
import image from './about-us-1056.jpg';
|
||||
|
||||
const AboutPage = () => {
|
||||
const { siteTwitterHandle, siteFacebookPage } = config;
|
||||
|
|
|
|||
BIN
src/containers/AboutPage/about-us-1056.jpg
Normal file
|
After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 846 KiB |
|
|
@ -75,11 +75,13 @@ export const withViewport = Component => {
|
|||
* the shape `{ width: 600, height: 400}`.
|
||||
*
|
||||
* @param {React.Component} Component to be wrapped by this HOC
|
||||
* @param {Object} options pass in options like maxWidth and maxHeight.
|
||||
* @param {Object} options pass in options like maxWidth and maxHeight. To load component after
|
||||
* initial rendering has passed or after user has interacted with the window (e.g. scrolled),
|
||||
* use`loadAfterInitialRendering: 1500` (value should be milliseconds).
|
||||
*
|
||||
* @return {Object} HOC component which knows its dimensions
|
||||
*/
|
||||
export const lazyLoadWithDimensions = (Component, options) => {
|
||||
export const lazyLoadWithDimensions = (Component, options = {}) => {
|
||||
// The resize event is flooded when the browser is resized. We'll
|
||||
// use a small timeout to throttle changing the viewport since it
|
||||
// will trigger rerendering.
|
||||
|
|
@ -96,6 +98,7 @@ export const lazyLoadWithDimensions = (Component, options) => {
|
|||
super(props);
|
||||
this.element = null;
|
||||
this.defaultRenderTimeout = null;
|
||||
this.afterRenderTimeout = null;
|
||||
|
||||
this.state = { width: 0, height: 0 };
|
||||
|
||||
|
|
@ -112,6 +115,15 @@ export const lazyLoadWithDimensions = (Component, options) => {
|
|||
this.defaultRenderTimeout = window.setTimeout(() => {
|
||||
if (this.isElementNearViewport(0)) {
|
||||
this.setDimensions();
|
||||
} else {
|
||||
const loadAfterInitialRendering = options.loadAfterInitialRendering;
|
||||
if (typeof loadAfterInitialRendering === 'number') {
|
||||
this.afterRenderTimeout = window.setTimeout(() => {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.setDimensions();
|
||||
});
|
||||
}, loadAfterInitialRendering);
|
||||
}
|
||||
}
|
||||
}, RENDER_WAIT_MS);
|
||||
}
|
||||
|
|
@ -121,11 +133,18 @@ export const lazyLoadWithDimensions = (Component, options) => {
|
|||
window.removeEventListener('resize', this.handleWindowResize);
|
||||
window.removeEventListener('orientationchange', this.handleWindowResize);
|
||||
window.clearTimeout(this.defaultRenderTimeout);
|
||||
|
||||
if (this.afterRenderTimeout) {
|
||||
window.clearTimeout(this.afterRenderTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
handleWindowResize() {
|
||||
if (this.isElementNearViewport(NEAR_VIEWPORT_MARGIN)) {
|
||||
this.setDimensions();
|
||||
const shouldLoadToImproveScrolling = typeof options.loadAfterInitialRendering === 'number';
|
||||
if (this.isElementNearViewport(NEAR_VIEWPORT_MARGIN) || shouldLoadToImproveScrolling) {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.setDimensions();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -159,7 +178,10 @@ export const lazyLoadWithDimensions = (Component, options) => {
|
|||
const { maxWidth, maxHeight } = options;
|
||||
const maxWidthMaybe = maxWidth ? { maxWidth } : {};
|
||||
const maxHeightMaybe = maxHeight ? { maxHeight } : {};
|
||||
const style = { width: '100%', height: '100%', ...maxWidthMaybe, ...maxHeightMaybe };
|
||||
const style =
|
||||
maxWidth || maxHeight
|
||||
? { width: '100%', height: '100%', ...maxWidthMaybe, ...maxHeightMaybe }
|
||||
: { position: 'absolute', top: 0, right: 0, bottom: 0, left: 0 };
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
|||