Repository: mdn/kuma

Issue:
We should use JSON.parse instead of a JS literal for hydration state
See https://v8.dev/blog/cost-of-javascript-2019#json

We're currently doing this...
```
<script>window._react_data = {"locale": "en-US", "stringCatalog": {}, ... </script>
```

We should be doing...
```
<script>window._react_data = JSON.parse('{"locale": "en-US", "stringCatalog": {}, ... ')</script>
```



Discussion:
I made a prototype that does this and compared:
https://www.peterbe.com/specificity_offline/beta/index.html 

<img width="1077" alt="Screen Shot 2019-08-27 at 8 12 16 AM" src="https://user-images.githubusercontent.com/26739/63770164-8a2d6a00-c8a2-11e9-8cb1-9ae5e8e3be28.png">


...vs...

https://www.peterbe.com/specificity_offline/beta_json_parse_data/index.html


<img width="1112" alt="Screen Shot 2019-08-27 at 8 12 24 AM" src="https://user-images.githubusercontent.com/26739/63770177-90bbe180-c8a2-11e9-9c04-d6a89d10dd48.png">


Definitely a win. But a bit small. 

Resolving patch:
diff --git a/kuma/wiki/templatetags/ssr.py b/kuma/wiki/templatetags/ssr.py
--- a/kuma/wiki/templatetags/ssr.py
+++ b/kuma/wiki/templatetags/ssr.py
@@ -52,47 +52,22 @@ def render_react(component_name, locale, url, document_data, ssr=True):
         return client_side_render(component_name, data)
 
 
-def _render(component_name, html, state):
+def _render(component_name, html, script, needs_serialization=False):
     """A utility function used by both client side and server side rendering.
     Returns a string that includes the specified HTML and a serialized
     form of the state dict, in the format expected by the client-side code
     in kuma/javascript/src/index.jsx.
     """
-    # We're going to need this below, but we don't want to keep it around
-    pluralExpression = state['pluralExpression']
-    del state['pluralExpression']
-
-    # Serialize the state object to JSON and be sure the string
-    # "</script>" does not appear in it, since we are going to embed it
-    # within an HTML <script> tag.
-    serializedState = json.dumps(state).replace('</', '<\\/')
-
-    # In addition to the JSON-serialized data structure, we also want
-    # to pass the pluralForm() function required for the ngettext()
-    # localization function. Functions can't be included in JSON, but
-    # they are part of JavaScript, and our serializedState string is
-    # embedded in an HTML <script> tag, so it can include arbitrary
-    # JavaScript, not just JSON. The reason that we need to do this
-    # is that Django provides us with a JS expression as a string and
-    # we need to convert it into JS code. If we don't do it here with
-    # string manipulation, then we need to use eval() or `new Function()`
-    # on the client-side and that causes a CSP violation.
-    if pluralExpression:
-        # A JavaScript function expression as a Python string
-        js_function_text = (
-            'function(n){{var v=({});return(v===true)?1:((v===false)?0:v);}}'
-            .format(pluralExpression)
-        )
-        # Splice it into the JSON-formatted data string
-        serializedState = (
-            '{pluralFunction:' + js_function_text + ',' + serializedState[1:]
-        )
-
-    # Now return the HTML and the state as a single string
+    if needs_serialization:
+        assert isinstance(script, dict), type(script)
+        script = json.dumps(script).replace('</', '<\\/')
+    else:
+        script = u'JSON.parse({})'.format(script)
+
     return (
         u'<div id="react-container" data-component-name="{}">{}</div>\n'
         u'<script>window._react_data = {};</script>\n'
-    ).format(component_name, html, serializedState)
+    ).format(component_name, html, script)
 
 
 def client_side_render(component_name, data):
@@ -100,7 +75,7 @@ def client_side_render(component_name, data):
     Output an empty <div> and a script with complete state so that
     the UI can be rendered on the client-side.
     """
-    return _render(component_name, '', data)
+    return _render(component_name, '', data, needs_serialization=True)
 
 
 def server_side_render(component_name, data):
@@ -114,7 +89,6 @@ def server_side_render(component_name, data):
     """
     url = '{}/{}'.format(settings.SSR_URL, component_name)
     timeout = settings.SSR_TIMEOUT
-
     # Try server side rendering
     try:
         # POST the document data as JSON to the SSR server and we
@@ -156,8 +130,8 @@ def server_side_render(component_name, data):
         #     data['documentData'].update(bodyHTML='',
         #                                 tocHTML='',
         #                                 quickLinksHTML='')
-
-        return _render(component_name, response.text, data)
+        result = response.json()
+        return _render(component_name, result['html'], result['script'])
 
     except requests.exceptions.ConnectionError:
         print("Connection error contacting SSR server.")


Test patch:
diff --git a/kuma/javascript/ssr-server.test.js b/kuma/javascript/ssr-server.test.js
--- a/kuma/javascript/ssr-server.test.js
+++ b/kuma/javascript/ssr-server.test.js
@@ -51,9 +51,9 @@ describe('ssr-server routes', () => {
             .post(`/ssr/${name}`)
             .send(data)
             .expect(200)
-            .expect('Content-Type', 'text/plain; charset=utf-8')
+            .expect('Content-Type', 'application/json; charset=utf-8')
             .expect(response => {
-                expect(response.text).toBe(mockssr(name, data));
+                expect(response.body).toBe(mockssr(name, data));
             });
     });
 });
diff --git a/kuma/wiki/tests/test_ssr.py b/kuma/wiki/tests/test_ssr.py
--- a/kuma/wiki/tests/test_ssr.py
+++ b/kuma/wiki/tests/test_ssr.py
@@ -51,7 +51,9 @@ def test_server_side_render(mock_get_l10n_data, mock_dumps, locale,
 
     url = '{}/{}'.format(settings.SSR_URL, 'document')
 
-    mock_requests.post(url, text=mock_html)
+    mock_requests.post(url, json={
+        'html': mock_html,
+        'script': 'STUFF'})
 
     # Run the template tag
     path = '/en-US/docs/foo'
@@ -64,42 +66,11 @@ def test_server_side_render(mock_get_l10n_data, mock_dumps, locale,
         'stringCatalog': localization_data['catalog'],
         'documentData': document_data,
     }
-    assert output == (
+    expect = (
         u'<div id="react-container" data-component-name="{}">{}</div>\n'
-        u'<script>window._react_data = {};</script>\n'
+        u'<script>window._react_data = JSON.parse(STUFF);</script>\n'
     ).format('document', mock_html, json.dumps(data))
-
-
-@mock.patch('json.dumps')
-@mock.patch('kuma.wiki.templatetags.ssr.get_localization_data')
-def test_plural_function(mock_get_l10n_data, mock_dumps,
-                         mock_requests, settings):
-    """For server-side rendering, if the locale data includes a plural
-       expression, expect the output to include a plural function.
-    """
-
-    mock_dumps.side_effect = sorted_json_dumps
-
-    # This is the input to the mock Node server
-    document_data = {'x': 'one', 'y': 2, 'z': ['a', 'b']}
-
-    localization_data = {'catalog': {'s': 't'}, 'plural': 'n!=1'}
-    mock_get_l10n_data.side_effect = lambda l: localization_data
-
-    url = '{}/{}'.format(settings.SSR_URL, 'page')
-
-    # This will be the output sent by the mock Node server
-    mock_requests.post(url, text='mock html')
-
-    # Run the template tag
-    path = '/en-US/docs/foo'
-    output = ssr.render_react('page', 'es', path, document_data)
-
-    expected = '<script>window._react_data = {pluralFunction:function(n){'
-
-    # Make sure the output is as expected
-    assert expected in output
-    assert localization_data['plural'] in output
+    assert output == expect
 
 
 @mock.patch('json.dumps')
@@ -117,12 +88,14 @@ def test_client_side_render(mock_get_l10n_data, mock_dumps):
         'url': path,
         'stringCatalog': localization_data['catalog'],
         'documentData': document_data,
+        'pluralExpression': None,
     }
     output = ssr.render_react('page', 'en-US', path, document_data, ssr=False)
-    assert output == (
+    expected = (
         u'<div id="react-container" data-component-name="{}"></div>\n'
         u'<script>window._react_data = {};</script>\n'
     ).format('page', json.dumps(data))
+    assert output == expected
 
 
 @pytest.mark.parametrize('failure_class', [