20220214のReactに関する記事は8件です。

英語で5歳児みたいなことしか言えないのが悔しくてAI添削英語日記アプリを作りました【個人開発】

概要 「英語で話す時に5歳児みたいなことしか言えなくなる問題」を解決するために、DeepLを用いたAIが添削してくれる英語日記サービスを作りました。 毎日このWebサービスで英語日記を書くと、自分では思い付かない表現などが知れて、少しずつフレーズの引き出しを増やすことが出来た実感があります。 毎日続けていくことで、普段使う言葉で英語を話せるようになるのではないかと思います。 React・Firebase・GCP・DeepL APIを用いて開発しています。 課題「英語で話す時に5歳児みたいなことしか言えなくなる問題」 まずこの5歳児というキーワードを見た時に、Qiitaを見ている多くの方は下記の記事を思い浮かべたんじゃないかと思います。 自分もこの記事を見て、感銘を受けたうちの一人です。めちゃくちゃ素晴らしいサービスだなと思います。自分も一応、TOEIC810点でオンライン英会話やってたこともあり、日常英会話ならなんとかなるレベルです。ただし、少し複雑な事を英語で言おうとすると詰まってしまう場面があり、非常に共感できました。 ただ同時に、この記事を読んで僕がずっとやりたかったことが、実現できるんじゃないかとも思いました。 解決策「英語日記×DeepL」 Englisterさんは、上記の課題に対して英作文×DeepLという解決策を提示されています。 自分は、よりニッチに英語日記×DeepLという解決策を提示したいと思います。 英語日記は英語日記BOYという本で話題になりましたが、英語を話せるようになる上で非常に有用な学習方法だと信じています。 下図は、自分が英語日記を始めてからWebサービスを実際に開発して試すまでの過程です。 自分のように英語日記に挫折した経験ある方でも、続けられるようなサービスを作りました。 作ったもの LDiary 英語が話せるようになるAI英語日記アプリ URL 使い方 使い方としては以下のようなステップで英語日記を書いていきます。 いきなり英語で書かないため、英語日記に挫折した経験のある方も継続しやすいと思います。 1. 日本語で日記を書く 2. 英語で日記を訳してみる 3. AIのお手本の英語と比較して覚える 4. 日記を登録する STEP1 日本語で日記を書く まずは日本語で日記を書いていきます。 自然ないつも通りの日本語で書くのがポイントです。 STEP2 英語で日記を訳してみる 次に、先ほど書いた日本語の日記を英語に訳していきます。 この時、あまり間違いを気にせず自分の中で出てきた単語でサクッと書いていくことが続けるコツだと思います。 STEP3 AIのお手本の英語と比較して覚える AIがお手本の英語を提案してくれます。 自分の文章と見比べて、より自然な表現などを学べます。 人によると思いますが、自分はお手本の文章を何回かブツブツ言って覚えるようにしています。 STEP4 日記を登録する 最後に日記を登録します。 英語日記は毎日書いてこそ効果があると思いますので、継続できるように目標が設定されています。 他にもダッシュボードで点数の履歴や、どれくらい継続できているかなどを見ることができます。 効果はある? 毎日やっていると、普段自分が使う言葉は英語ではこういう風に言うのか!というのが多くて非常に勉強になっています。 これを継続してやってフレーズを覚えていくことで、英語を喋れるようになれるだろうなという実感あります。 特に英語学習初中級者には最適な勉強法でないでしょうか。 技術的な話 構成 今回は下記のような構成で作成しています。MUIのテンプレートをいじってデザインにかかる時間を短縮しています。 フロントエンド React MUI バックエンド Python プラットフォーム Firebase(FireStore, Authentication, Hosting ) GCP(Functions, Storage) 苦労した点 今回Pythonのライブラリを使いたかったのですが、Firebase FunctionsではNode.jsしか使用できないので、あえてGCPのFunctionsを使っています。 一応、GCPのFunctionsに認証をつけて呼び出せるようにしているのですがここに時間がかかりました。 認証部分にはFirebase Authenticationのトークンを検証する形で実装しています。 最初はGCPのAPI Gateway + FuncitonsでAPI Gatewayの部分に検証を任せようと思いましたがこれが上手くいきませんでした。 トークンの検証自体は上手くいったのですが、CORSのエラーが解消できませんでした。どうやらサポートされていないみたいです。 こちらの最初の回答のようにAPI Gatewayのyamlにoptionsメソッドを追加してみてもダメでしたね。 最終的にはこちらを参考にFirebaseに直接認証を実装しました。 正解かは分かりませんが、以下のような形でPythonで実装しました。 自分が詳しくないので、もう少し良い方法があればぜひ教えて頂けたら嬉しいです。 import json import firebase_admin from firebase_admin import credentials from firebase_admin import auth from google.cloud import storage def verify_token(request): # appsが初期化済みか判定 if (not len(firebase_admin._apps)): # ストレージからサービスアカウント.jsonを取得 storage_client = storage.Client() bucket = storage_client.bucket('バケット名') blob = bucket.blob('ファイル名') # 文字列として取得 read_output = blob.download_as_string() # json文字列をdictへ変換 dict_data = json.loads(read_output) # SDKを初期化 cred = credentials.Certificate(dict_data) firebase_admin.initialize_app(cred) # リクエストからトークンを取得する header = request.headers.get("Authorization", None) PREFIX = 'Bearer ' id_token = header[len(PREFIX):] # トークンの認証 decoded_token = auth.verify_id_token(id_token) uid = decoded_token['uid'] # 認証後に以下の処理を実行する まとめ 英語を話すのがが苦手でもLDiaryで英語日記を継続することで、普段使う言葉で英語を話せるようになります。 登録なしで無料でも使えますので、ぜひ試してみてください! Twitterもやっていますので、良かったらフォローしてください!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】eslintの設定 備忘録

初めに React では eslint を使うのが一般的になってきています。その際に設定をいちいち考えるのなかなかめんどくさいので,忘備録としてここに残しておきます。 typescript・prettie 対応です。 eslintrc.js .eslintrc.js module.exports = { env: { browser: true, es2021: true, }, extends: [ 'plugin:react/recommended', 'airbnb', 'airbnb/hooks', 'plugin:import/errors', 'plugin:import/warnings', 'plugin:import/typescript', 'plugin:cypress/recommended', 'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended-requiring-type-checking', 'prettier', ], globals: { cy: 'readonly', Cypress: 'readonly', }, parser: '@typescript-eslint/parser', parserOptions: { ecmaFeatures: { jsx: true, }, ecmaVersion: 12, project: './tsconfig.eslint.json', sourceType: 'module', tsconfigRootDir: __dirname, }, plugins: [ '@typescript-eslint', 'import', 'jsx-a11y', 'prefer-arrow', 'react', 'react-hooks', 'cypress', ], root: true, rules: { 'no-use-before-define': 'off', '@typescript-eslint/no-use-before-define': ['error'], 'lines-between-class-members': [ 'error', 'always', { exceptAfterSingleLine: true, }, ], 'no-void': [ 'error', { allowAsStatement: true, }, ], 'padding-line-between-statements': [ 'error', { blankLine: 'always', prev: '*', next: 'return', }, ], '@typescript-eslint/no-unused-vars': [ 'error', { vars: 'all', args: 'after-used', argsIgnorePattern: '_', ignoreRestSiblings: false, varsIgnorePattern: '_', }, ], 'import/extensions': [ 'error', 'ignorePackages', { js: 'never', jsx: 'never', ts: 'never', tsx: 'never', }, ], 'import/no-extraneous-dependencies': [ 'error', { devDependencies: [ '**/setupTests.ts', '.storybook/**', 'stories/**', '**/*/*.story.*', '**/*/*.stories.*', '**/__specs__/**', '**/*/*.spec.*', '**/__tests__/**', '**/*/*.test.*', ], }, ], 'prefer-arrow/prefer-arrow-functions': [ 'error', { disallowPrototype: true, singleReturnOnly: false, classPropertiesAllowed: false, }, ], 'react/jsx-filename-extension': [ 'error', { extensions: ['.jsx', '.tsx'], }, ], 'react/jsx-props-no-spreading': [ 'error', { html: 'enforce', custom: 'enforce', explicitSpread: 'ignore', }, ], 'react/react-in-jsx-scope': 'off', 'react-hooks/rules-of-hooks': 'error', 'react/require-default-props': [0], 'react-hooks/exhaustive-deps': [ 'warn', { additionalHooks: 'useRecoilCallback', }, ], }, overrides: [ { files: ['*.tsx'], rules: { 'react/prop-types': 'off', }, }, ], settings: { 'import/resolver': { node: { paths: ['src/'], extensions: ['.js', '.jsx', '.ts', '.tsx'], }, }, }, }; .eslintignore .eslintignore build/ public/ **/coverage/ **/node_modules/ **/*.min.js *.config.js .*lintrc.js .eslintrc.js cypress/plugins/ cypress/integration/ cypress/support/ .stylelintrc.js .styleintrc.js module.exports = { extends: ['stylelint-config-standard', 'stylelint-config-recess-order'], plugins: ['stylelint-order'], ignoreFiles: ['**/node_modules/**'], rules: { 'string-quotes': 'single', }, }; .prettierrc .prettierrc singleQuote: true trailingComma: "all" tsconfig.eslint.json tsconfig.eslint.json { "extends": "./tsconfig.json", "include": [ "src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx", ], "exclude": [ "node_modules" ] } tsconfig.json tsconfig.json { "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext", "es5" ], "types": ["jest"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", "baseUrl": "src", "downlevelIteration": true }, "include": [ "src", ] } package.json package.json { "name": "coffee-oma", "version": "0.1.0", "private": true, "dependencies": { "@hookform/error-message": "^2.0.0", "@storybook/react": "^6.2.9", "@testing-library/jest-dom": "^5.11.10", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^13.0.16", "@types/jest": "^26.0.22", "@types/node": "^14.14.37", "@types/react": "^17.0.3", "@types/react-dom": "^17.0.3", "@types/react-router": "^5.1.18", "@types/react-router-dom": "^5.3.3", "@types/recoil": "^0.0.1", "axios": "^0.21.1", "browserslist": "^4.18.0", "dayjs": "^1.10.4", "eslint-config-react-app": "^6.0.0", "framer-motion": "^4.1.17", "history": "5", "react": "^17.0.2", "react-cookie": "^4.1.1", "react-docgen-typescript-webpack-plugin": "^1.1.0", "react-dom": "^17.0.2", "react-helmet": "^6.1.0", "react-hook-form": "^7.4.0", "react-loader-spinner": "^4.0.0", "react-query": "^3.32.1", "react-rating": "^2.0.5", "react-router": "6.2.1", "react-router-dom": "6.2.1", "react-scripts": "4.0.3", "react-simple-star-rating": "^4.0.0", "recoil": "^0.2.0", "semantic-ui-css": "^2.4.1", "semantic-ui-react": "^2.0.3", "styled-components": "^5.2.3", "typescript": "^4.2.3", "use-file-upload": "^1.0.9", "use-state-if-mounted": "^1.0.4", "web-vitals": "^1.1.1" }, "scripts": { "start": "REACT_APP_NODE_ENV=development react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", "heroku:start": "react-scripts build && react-scripts start", "fix": "npm run -s format && npm run -s lint:fix", "format": "prettier --write --loglevel=warn 'src/**/*.{js,jsx,ts,tsx,gql,graphql,json}'", "lint": "npm run -s lint:style; npm run -s lint:es", "lint:fix": "npm run -s lint:style:fix && npm run -s lint:es:fix", "lint:es": "eslint 'src/**/*.{js,jsx,ts,tsx}'", "lint:es:fix": "eslint --fix 'src/**/*.{js,jsx,ts,tsx}'", "lint:conflict": "eslint-config-prettier 'src/**/*.{js,jsx,ts,tsx}'", "lint:style": "stylelint 'src/**/*.{css,less,sass,scss}'", "lint:style:fix": "stylelint --fix 'src/**/*.{css,less,sass,scss}'", "preinstall": "typesync || :", "storybool": "start-storybook -p 6006", "build:storybook": "build-storybook -o build/stroybook", "storybook": "start-storybook -p 6006 -s public", "build-storybook": "build-storybook -s public", "cypress": "cypress open", "cypress:run": "cypress run", "start:ci": "CHOKIDAR_USEPOLLING=true react-scripts start", "cy:ci": "start-server-and-test start http://localhost:3000 cypress:run" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ], "overrides": [ { "files": [ "**/*.stories.*" ], "rules": { "import/no-anonymous-default-export": "off" } } ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "jest": { "resetMocks": false }, "devDependencies": { "@storybook/addon-a11y": "^6.2.9", "@storybook/addon-actions": "^6.2.9", "@storybook/addon-console": "^1.2.3", "@storybook/addon-controls": "^6.2.9", "@storybook/addon-docs": "^6.2.9", "@storybook/addon-essentials": "^6.2.9", "@storybook/addon-info": "^5.3.21", "@storybook/addon-knobs": "^6.2.9", "@storybook/addon-links": "^6.2.9", "@storybook/addon-storyshots": "^6.2.9", "@storybook/addon-storyshots-puppeteer": "^6.2.9", "@storybook/addon-storysource": "^6.2.9", "@storybook/addon-viewport": "^6.2.9", "@storybook/node-logger": "^6.2.9", "@storybook/preset-create-react-app": "^3.1.7", "@types/babel-plugin-macros": "^2.8.4", "@types/prettier": "^2.2.3", "@types/react-helmet": "^6.1.1", "@types/react-test-renderer": "^17.0.1", "@types/storybook-react-router": "^1.0.1", "@types/storybook__addon-actions": "^5.2.1", "@types/storybook__addon-info": "^5.2.3", "@types/storybook__addon-knobs": "^5.2.1", "@types/storybook__react": "^5.2.1", "@types/styled-components": "^5.1.9", "@types/stylelint": "^9.10.1", "@types/testing-library__jest-dom": "^5.9.5", "@types/testing-library__user-event": "^4.2.0", "@typescript-eslint/eslint-plugin": "^4.20.0", "@typescript-eslint/parser": "^4.20.0", "axios-mock-adapter": "^1.20.0", "babel-loader": "^8.2.2", "babel-plugin-macros": "^3.1.0", "cypress": "^8.7.0", "eslint-config-airbnb": "18.2.1", "eslint-config-prettier": "^8.1.0", "eslint-plugin-cypress": "^2.12.1", "eslint-plugin-import": "^2.22.1", "eslint-plugin-jsx-a11y": "^6.4.1", "eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-react": "^7.23.1", "eslint-plugin-react-hooks": "4.2.0", "jest": "^27.0.1", "jest-localstorage-mock": "^2.4.13", "jest-watch-typeahead": "^0.6.4", "prettier": "^2.2.1", "puppeteer": "^10.0.0", "react-docgen-typescript-loader": "^3.7.2", "react-test-renderer": "^17.0.2", "require-context.macro": "^1.2.2", "start-server-and-test": "^1.14.0", "storybook-addon-mock": "^0.1.0", "storybook-axios": "^1.0.1", "storybook-react-router": "^1.0.8", "stylelint": "^13.12.0", "stylelint-config-recess-order": "^2.3.0", "stylelint-config-standard": "^21.0.0", "stylelint-order": "^4.1.0", "typescript-styled-plugin": "^0.15.0", "typesync": "^0.8.0" } } 参考 下記の書籍を読んで学べば、React の事は完全に理解できると思ういます。 りあクト
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React: react-router-dom (v6) の使い方

こちらのページを参考にしました。 React Router v6 はじめでもわかるルーティングの設定方法の基礎 次のようーなページを作成します。 ホーム 紹介 連絡先 プロジェクトの作成 npx create-react-app proj01 インストールされたモジュールの確認 cd proj01 $ npm list --depth=0 proj01@0.1.0 /home/uchida/tmp/feb14/proj01 ├── @testing-library/jest-dom@5.16.2 ├── @testing-library/react@12.1.2 ├── @testing-library/user-event@13.5.0 ├── react-dom@17.0.2 ├── react-scripts@5.0.0 ├── react@17.0.2 └── web-vitals@2.1.4 react-router-dom をインストール npm install react-router-dom バージョンを確認 $ npm list --depth=0 proj01@0.1.0 /home/uchida/tmp/feb14/proj01 ├── @testing-library/jest-dom@5.16.2 ├── @testing-library/react@12.1.2 ├── @testing-library/user-event@13.5.0 ├── react-dom@17.0.2 ├── react-router-dom@6.2.1 ├── react-scripts@5.0.0 ├── react@17.0.2 └── web-vitals@2.1.4 次のプログラムを差し替えます。 src/App.js src/index.js src/App.js import { Routes, Route } from 'react-router-dom'; import Home from './routes/home'; import About from './routes/about'; import Contact from './routes/contact'; import NoMatch from './routes/nomatch'; function App() { return ( <div className="App"> <h1>Hello React Router v6</h1> <ul> <li> <a href="/">ホーム</a> </li> <li> <a href="/about">紹介</a> </li> <li> <a href="/contact">連絡先</a> </li> </ul> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> <Route path="/contact" element={<Contact />} /> <Route path="*" element={<NoMatch />} /> </Routes> </div> ); } export default App; src/index.js import ReactDOM from 'react-dom'; import App from './App'; import { BrowserRouter } from 'react-router-dom'; ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById('root') ); 次のコードを作成します。 src/routes/home.js src/routes/about.js src/routes/contact.js mkdir src/routes src/routes/home.js function Home() { return <h2>トップページ</h2>; } export default Home; src/routes/about.js function About() { return <h2>このページについて</h2>; } export default About; src/routes/contact.js function Contact() { return <h2>連絡先のメールアドレス</h2>; } export default Contact; サーバーの起動 npm start ブラウザーで、 http://localhost:3000 にアクセス
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 03 ー 構成とロジックを整える

Recoil公式チュートリアルの解説は前回で終えました。この第3回はスピンオフです。とくに動きは変えることなく、モジュールの構成とロジックを整理します。せっかくモジュール分けもしましたので、もう少し実践に近づけようという試みです。 React + TypeScript: Recoil tutorial example 03 ディレクトリをコンポーネントと状態で分ける 簡単な作例ながら、それなりにモジュール数も増えました。そこで、コンポーネント(components/)と状態(state/)を、つぎのようにディレクトリ分けすることにします。 src/components/ App.tsx TodoItem.tsx TodoItemCreator.tsx TodoList.tsx TodoListFilters.tsx TodoListStats.tsx src/state/ filteredTodoListState.ts todoListFilterState.ts todoListState.ts todoListStatsState.ts すると、各モジュールがimportするパスを修正しなければなりません。機械的な書き替えですので、コードを羅列します。動きが変わらず、エラーの出ないことを確かめてください。 src/index.tsx // import App from './App'; import App from './components/App'; src/components/TodoItem.tsx // import { todoListState, TodoItemType } from './todoListState'; import { todoListState, TodoItemType } from '../state/todoListState'; src/components/TodoItemCreator.tsx // import { todoListState } from './todoListState'; import { todoListState } from '../state/todoListState'; src/components/TodoList.tsx // import { filteredTodoListState } from './filteredTodoListState'; import { filteredTodoListState } from '../state/filteredTodoListState'; src/components/TodoListFilters.tsx // import { todoListFilterState } from './todoListFilterState'; import { todoListFilterState } from '../state/todoListFilterState'; src/components/TodoListStats.tsx // import { todoListStatsState } from './todoListStatsState'; import { todoListStatsState } from '../state/todoListStatsState'; フィルタ設定のロジックをコンポーネントから状態に移す フィルタの設定はコンポーネント(TodoListFilters)から行うのでなく、状態(todoListFilterState)にカスタムフック(useFilter)として移します。コードはむしろ増えるものの、フックを介すことによりコンポーネントが直に状態を変えることがなくなるのです。そのため、コンポーネントが用いるRecoilのフックも、値を参照するだけのuseRecoilValueに差し替えました。 src/state/todoListFilterState.ts import { useCallback } from 'react'; // import { atom } from 'recoil'; import { atom, useSetRecoilState } from 'recoil'; export const useFilter = () => { const setFilter = useSetRecoilState(todoListFilterState); const setListFilter = useCallback( (filter: string) => { setFilter(filter); }, [setFilter] ); return { setListFilter }; }; src/components/TodoListFilters.tsx // import { useRecoilState } from 'recoil'; import { useRecoilValue } from 'recoil'; // import { todoListFilterState } from '../state/todoListFilterState'; import { todoListFilterState, useFilter } from '../state/todoListFilterState'; export const TodoListFilters: VFC = () => { // const [filter, setFilter] = useRecoilState(todoListFilterState); const filter = useRecoilValue(todoListFilterState); const { setListFilter } = useFilter(); const updateFilter: ChangeEventHandler<HTMLSelectElement> = useCallback( ({ target: { value } }) => { // setFilter(value); setListFilter(value); }, // [setFilter] [setListFilter] ); }; Todoリストへの項目データ追加をコンポーネントから状態に移す Todoリストに項目を追加するコンポーネント(TodoItemCreator)のロジックも、同じように状態(todoListState)のフック(useTodoList)に移します。すると、コンポーネントは状態を直に参照することさえなくなるのです。 src/state/todoListState.ts import { useCallback } from 'react'; // import { atom } from 'recoil'; import { atom, useSetRecoilState } from 'recoil'; let id = 0; function getId() { return id++; } export const useTodoList = () => { const setTodoList = useSetRecoilState(todoListState); const addListItem = useCallback( (text: string) => { setTodoList((oldTodoList) => [ ...oldTodoList, { id: getId(), text, isComplete: false, }, ]); }, [setTodoList] ); return { addListItem }; }; src/components/TodoItemCreator.tsx // import { useSetRecoilState } from 'recoil'; // import { todoListState } from '../state/todoListState'; import { useTodoList } from '../state/todoListState'; export const TodoItemCreator: VFC = () => { // const setTodoList = useSetRecoilState(todoListState); const { addListItem } = useTodoList(); const addItem = useCallback(() => { /* setTodoList((oldTodoList) => [ ...oldTodoList, { id: getId(), text: inputValue, isComplete: false, }, ]); */ addListItem(inputValue); // }, [inputValue, setTodoList]); }, [addListItem, inputValue]); }; /* let id = 0; function getId() { return id++; } */ Todoリストの項目編集・チェック・削除の操作をコンポーネントでなく状態に備える Todoリストの各項目を編集・チェック・削除するTodoItemは、もっともロジックの多いコンポーネントです。操作を順に状態(todoListState)に移しましょう。 Todoリストの項目をフックで編集する まず、Todoリスト項目の編集です。コンポーネント(TodoItem)から状態(todoListState)のフック(useTodoList)に移します。カスタムフックから状態の設定だけでなく参照もできるように、RecoilのuseRecoilStateを用いなければなりません。 src/state/todoListState.ts // import { atom, useSetRecoilState } from 'recoil'; import { atom, useRecoilState } from 'recoil'; function replaceItemAtIndex( arr: TodoItemType[], index: number, newValue: TodoItemType ) { return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)]; } export const useTodoList = () => { // const setTodoList = useSetRecoilState(todoListState); const [todoList, setTodoList] = useRecoilState(todoListState); const editItemTextAtIndex = useCallback( (index: number, item: TodoItemType, text: string) => { const newList = replaceItemAtIndex(todoList, index, { ...item, text, }); setTodoList(newList); }, [setTodoList, todoList] ); // return { addListItem }; return { addListItem, editItemTextAtIndex }; }; src/components/TodoItem.tsx // import { todoListState, TodoItemType } from '../state/todoListState'; import { todoListState, TodoItemType, useTodoList, } from '../state/todoListState'; export const TodoItem: VFC<Props> = ({ item }) => { const { editItemTextAtIndex } = useTodoList(); const editItemText: ChangeEventHandler<HTMLInputElement> = useCallback( ({ target: { value } }) => { /* const newList = replaceItemAtIndex(todoList, index, { ...item, text: value, }); setTodoList(newList); */ editItemTextAtIndex(index, item, value); }, // [index, item, setTodoList, todoList] [editItemTextAtIndex, index, item] ); }; Todo項目のチェックを切り替える つぎは、Todo項目のチェックの切り替えです。コンポーネント(TodoItem)から状態(todoListState)のフック(useTodoList)に関数(toggleItemCompletionAtIndex)を移します。 src/state/todoListState.ts export const useTodoList = () => { const toggleItemCompletionAtIndex = useCallback( (index: number, item: TodoItemType) => { const newList = replaceItemAtIndex(todoList, index, { ...item, isComplete: !item.isComplete, }); setTodoList(newList); }, [setTodoList, todoList] ); // return { addListItem, editItemTextAtIndex }; return { addListItem, editItemTextAtIndex, toggleItemCompletionAtIndex }; }; src/components/TodoItem.tsx export const TodoItem: VFC<Props> = ({ item }) => { // const { editItemTextAtIndex } = useTodoList(); const { editItemTextAtIndex, toggleItemCompletionAtIndex } = useTodoList(); const toggleItemCompletion = useCallback(() => { /* const newList = replaceItemAtIndex(todoList, index, { ...item, isComplete: !item.isComplete, }); setTodoList(newList); */ toggleItemCompletionAtIndex(index, item); // }, [index, item, setTodoList, todoList]); }, [index, item, toggleItemCompletionAtIndex]); }; /* function replaceItemAtIndex( arr: TodoItemType[], index: number, newValue: TodoItemType ) { return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)]; } */ Todoリストから項目を削除する 最後は、Todoリストからの項目の削除です。コンポーネント(TodoItem)のロジックを、状態(todoListState)のフック(todoListState)に移します。もはやコンポーネントから状態を書き替える必要がありません。用いるRecoilのフックは、読み取り専用のuseRecoilValueに改めました。これで、すべてのコンポーネントからRecoilの状態は参照するのみで、値を直に書き替えることはなくなったのです。 src/state/todoListState.ts function removeItemAtIndex(arr: TodoItemType[], index: number) { return [...arr.slice(0, index), ...arr.slice(index + 1)]; } export const useTodoList = () => { const deleteItemAtIndex = useCallback( (index: number) => { const newList = removeItemAtIndex(todoList, index); setTodoList(newList); }, [setTodoList, todoList] ); // return { addListItem, editItemTextAtIndex, toggleItemCompletionAtIndex }; return { addListItem, deleteItemAtIndex, editItemTextAtIndex, toggleItemCompletionAtIndex, }; }; src/components/TodoItem.tsx // import { useRecoilState } from 'recoil'; import { useRecoilValue } from 'recoil'; export const TodoItem: VFC<Props> = ({ item }) => { // const [todoList, setTodoList] = useRecoilState(todoListState); const todoList = useRecoilValue(todoListState); // const { editItemTextAtIndex, toggleItemCompletionAtIndex } = useTodoList(); const { deleteItemAtIndex, editItemTextAtIndex, toggleItemCompletionAtIndex, } = useTodoList(); const deleteItem = useCallback(() => { /* const newList = removeItemAtIndex(todoList, index); setTodoList(newList); */ deleteItemAtIndex(index); // }, [index, setTodoList, todoList]); }, [deleteItemAtIndex, index]); }; /* function removeItemAtIndex(arr: TodoItemType[], index: number) { return [...arr.slice(0, index), ...arr.slice(index + 1)]; } */ フィルタの値の定数化と型定義 フィルタに設定する値は、3つの文字列だけです。だとすれば、定数にしてしまえると、管理しやすくスペルミスも防げます。ところが、標準JavaScriptでconst宣言したオブジェクトは、上書きができないだけです。それぞれのプロパティ値は書き替えられてしまいます。そいうとき、オブジェクトにconstアサーションを加えると、各プロパティも読み取り専用になるのです(「constアサーション「as const」 (const assertion)」参照) src/state/todoListFilterState.ts const FilterValue = { SHOW_ALL: 'Show All', SHOW_COMPLETED: 'Show Completed', SHOW_UNCOMPLETED: 'Show Uncompleted' } as const; さらに、3つの値しかとれない型が定められるならより安全になります。TypeScriptの演算子keyofとtypeofを組み合わせれば、そのようなユニオン型がつくれるのです(「オブジェクトからキーの型を生成する」および「TypeScriptの『typeof X[keyof typeof X]』の意味を順を追って理解する」参照)。 src/state/todoListFilterState.ts type FilterType = typeof FilterValue[keyof typeof FilterValue]; // つぎのユニオン型になる // type FilterType = 'Show All' | 'Show Completed' | 'Show Uncompleted' 改めて、フィルタの状態のモジュール(src/state/todoListFilterState.ts)は、つぎのように書き直します。 src/state/todoListFilterState.ts export const FilterValue = { SHOW_ALL: 'Show All', SHOW_COMPLETED: 'Show Completed', SHOW_UNCOMPLETED: 'Show Uncompleted' } as const; export type FilterType = typeof FilterValue[keyof typeof FilterValue]; // export const todoListFilterState = atom<string>({ export const todoListFilterState = atom<FilterType>({ // default: 'Show All', default: FilterValue.SHOW_ALL, }); export const useFilter = () => { const setListFilter = useCallback( // (filter: string) => { (filter: FilterType) => { }, ); }; フィルタを選択するコンポーネントも、importした型(FilterType)と値(FilterValue)を使って書き改めます。フィルタの状態に設定する(setListFilterの引数)値の型は、これまでのstringでは合わなくなりましたので、FilterTypeで型アサーションしなければなりません。 src/components/TodoListFilters.tsx // import { todoListFilterState, useFilter } from '../state/todoListFilterState'; import { FilterType, FilterValue, todoListFilterState, useFilter, } from '../state/todoListFilterState'; export const TodoListFilters: VFC = () => { const updateFilter: ChangeEventHandler<HTMLSelectElement> = useCallback( ({ target: { value } }) => { // setListFilter(value); setListFilter(value as FilterType); }, [setListFilter] ); return ( <> <select value={filter} onChange={updateFilter}> {/* <option value="Show All">All</option> */} <option value={FilterValue.SHOW_ALL}>All</option> {/* <option value="Show Completed">Completed</option> */} <option value={FilterValue.SHOW_COMPLETED}>Completed</option> {/* <option value="Show Uncompleted">Uncompleted</option> */} <option value={FilterValue.SHOW_UNCOMPLETED}>Uncompleted</option> </select> </> ); }; もうひとつだけ、Todoリスト項目がみずからを特定するために用いるキーのindexです。これは、useMemoフックでメモ化しておく方がよいでしょう。 src/components/TodoItem.tsx // import { ChangeEventHandler, useCallback, VFC } from 'react'; import { ChangeEventHandler, useCallback, useMemo, VFC } from 'react'; export const TodoItem: VFC<Props> = ({ item }) => { // const index = todoList.findIndex((listItem) => listItem === item); const index = useMemo( () => todoList.findIndex((listItem) => listItem === item), [item, todoList] ); }; フィルタの値は3つにかぎられ、その型も定められました。他のモジュールからの扱いがしやすく、安全になったでしょう。でき上がったTodoリストアプリケーションの各モジュールのコードは、冒頭に掲げたCodeSandboxのサンプルをご参照ください。 「React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 01 ー atomを使った項目操作」」 「React + TypeScript: Recoil公式チュートリアルのTodoリストをつくる 02 ー selectorによるフィルタリングと集計」
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

create-react-app できない!

事象 create-react-app しようとしたところ以下のように表示されます。 一見エラーっぽくないですがReactアプリは作られません。 $ npx create-react-app appsample_YYYYMMDD --template typescript Need to install the following packages: create-react-app Ok to proceed? (y) y You are running `create-react-app` 4.0.3, which is behind the latest release (5.0.0). We no longer support global installation of Create React App. Please remove any global installs with one of the following commands: - npm uninstall -g create-react-app - yarn global remove create-react-app The latest instructions for creating a new app can be found here: https://create-react-app.dev/docs/getting-started/ 以下のコマンドを実行するように書いてありますが、これらを実行してもうまくいきません。 - npm uninstall -g create-react-app - yarn global remove create-react-app 解決策 この方の記事が非常に参考になりました。 キャッシュが悪さをしているようです。 $ npx clear-npx-cache
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ストック推奨】Webフロントエンドパフォーマンスチューニング55選

こんにちは、ぬこすけです。 近年、Webフロントエンドではサイトのパフォーマンスの重要性が高まっています。 例えば、GoogleはCore Web Vitalというパフォーマンスに指標を検索結果のランキング要因に組み込みました。 また、近年の某企業が「パフォーマンスの改善に取り組んだ結果、セッション数〇%アップ、CVR〇%アップ...」などの事例は枚挙にいとまがないでしょう。 パフォーマンスチューニングするためには、定量的に計測してボトルネックを探すようなトップダウンなアプローチもあります。 しかしながら、時には千本ノック的にハウツーを片っ端から試していくボトムアップなアプローチも有効になることもあったり、日々のコーディングでパフォーマンスを意識したコードを書くことは大切でしょう。 この記事ではパフォーマンス最適化のハウツーを紹介します。 パフォーマンス改善の施策が思い浮かばない時やフロントエンドのスキルを磨きたい時に辞書的な役割を果たせれば良いかなーと思っています。 ※私は55選書いている所で燃え尽きました。私自身も今後も更新する予定ですが、この記事は皆さんで作り上げたいと思っています。誤りや他にもこういうのあるよ!、この記事にわかりやすい説明あるよ!などあれば編集リクエスト、またはコメントいただけると幸いです。 ※この記事を読んでいる方にはこれからフロントエンジニアになりたい方、駆け出しエンジニアの方もいると思います。正直、何言ってるかわからない部分が結構あると思います。ですが、私の経験則上、「あの時書いてあったことはこういうことか!」と後々になって理解することがよくありました。今はよくわからないかもしれませんが、とりあえずストックなりしておいて、数ヶ月後にこの記事を見返すとまた理解度も変わるのかなーと思います。 注意事項 一口にフロントエンドといっても、SSRやらSSGやらでサーバー側も関わってくることもあるので、バックエンド寄りも話も混じっているので悪しからず。 わかりやすくするためにカテゴリに分けしていますが、微妙なカテゴリ分けのものもあるので悪しからず。 中には具体的なハウツーというより考え方みたいなものも混じっているかもしれませんが悪しからず。 環境によって必ずしもパフォーマンスが改善されるとは限らないので悪しからず。 あくまでパフォーマンスの観点なので他の観点では最適となるとは限らないので悪しからず。例えば、IndexedDBを紹介していますが、Sarafi 15で脆弱性が見つかっています。 紹介するものには特定のブラウザでしかサポートされていないものもあるので悪しからず。 JavaScript編 複数の非同期処理はPromise.allを使う もし互いに依存関係のない複数の非同期処理を実行しているのならば、Promise.allを使うのも手です。 async function notUsePromiseAll() { console.log('Start!!'); const response1 = await fetch("https://example.com/api/1"); const response2 = await fetch("https://example.com/api/2"); console.log('End!!'); } async function usePromiseAll() { console.log('Start!!'); const [response1, response2] = await Promise.all([ fetch("https://example.com/api/1"), fetch("https://example.com/api/2"), ]); console.log('End!!'); } Promise.allはいずれかの非同期処理が失敗すると全ての非同期処理が中断されます。 中断されたくない場合はPromise.allSettledが使えます。 非同期処理を待たなくて良い場合は待たない コードを眺めてみて、非同期処理を待たなくて良いところは待たないようにしましよう。 具体的には、もしasync/await構文を使っているならawaitを使わないことです。 const sendErrorToServer = async (message) => { // サーバーにエラー情報を送る処理 }; console.log('何かエラーが起きた'); // 後続の処理はサーバーにエラー情報を送る処理とは関係ないので await をつけない sendErrorToServer('エラーです'); console.log('後続の処理'); 先に非同期処理を走らせておく 互いに依存関係のある複数の非同期処理を実行する場合でも、時間がかかる処理の方を先に走らせておくのも良いでしょう。 const response1Promise = requestLongTime(); // ... // 色々処理 // ... const response1 = await response1Promise; const response2 = await requestShortTime(); console.log(response1, response2); キー/バリューを頻繁に追加や削除する場合はMapを使う MDNにも記載がありますが、キー/バリューのペアを頻繁に追加や削除する場合はObjectよりもMapを使ったほうが最適です。 const nameAgeMap = new Map() nameAgeMap.set('Tom', 19) nameAgeMap.set('Nancy', 32) nameAgeMap.delete('Tom') nameAgeMap.delete('Nancy') ... 膨大な配列の検索はキー/バリューで JavaScriptというよりかはロジックの問題かもしれません。 膨大な配列を検索する場合はキー/バリューに変換してから検索した方が速いです。 const thousandsPeople = [ { name: 'Tom', age: 19 }, { name: 'Nancy', age: 32 }, // ...めちゃくちゃ多い ] // 時間かかる const myFriend = thousandsPeople.find(({ name }) => name === 'Tom'); console.log(`The age is ${myFriend.age}`); const thousandsPeopleMap = { 'Tom': 19, 'Nancy': 32, // ... } // こっちのほうが速い const myFriend2 = thousandsPeopleMap['Tom']; console.log(`The age is ${myFriend2.age}`); 関数の結果をキャッシュする 頻繁に同じ引数で関数を実行したり、重い処理を走らせるなら関数の結果をキャッシュするのも有効です。 次のようなデコレータ関数を作れば、関数の結果をキャッシュできます。 function cachingDecorator(func) { const cache = new Map(); return x => { if (!x) { return func(x) } if (cache.has(x)) { return cache.get(x); } const result = func(x); cache.set(x, result); return result; } } function heavyFuncNoCache(str) { // 重い処理 } const heavyFunc = cachingDecorator(heavyFuncNoCache); heavyFunc('hoge'); // キャッシュから結果が返却される heavyFunc('hoge'); requireではなくimportを使う JavaScriptのモジュールの読み込み方にはrequireとimportの2種類があります。 requireは同期的、importは非同期的にモジュールを読み込むので、importの方が良いでしょう。 Node.jsといったサーバーサイドでJavaScriptを記述する場合はrequireを使うことが多いと思いますが、バージョン14であればpackage.jsonだったりファイルの拡張子をmjsにしたりいじることでimportで読み込めます。 なお、Qiitaのこの記事がわかりやすいです。 フェッチにはKeep-Aliveを指定する 何度も同じドメインへアクセスするのであればkeep-aliveを指定することでフェッチ処理が短縮されます。 import axios from 'axios'; import { Agent as HttpAgent } from 'http'; import { Agent as HttpsAgent } from 'https'; const httpAgent = new HttpAgent({ keepAlive: true }); const httpsAgent = new HttpsAgent({ keepAlive: true }); const keepAliveAxios = axios.create({ httpAgent, httpsAgent, }); keepAliveAxios.get(...); 非同期の関数を使う Node.jsには同期/非同期で別で用意されている関数があったりします。 例えばファイルに書き込みをする関数にはfs.writeFileSyncとfs.writeFileがあります。 もしフロントエンドアプリケーションのビルド時などに静的ファイルを生成する必要がある場合、特段理由がなければfs.writeFileを使いましょう。 HTML/CSSなどリソース編 imgやiframe、linkタグなどにimportance属性を追加する imgやiframe、linkタグなどではimportance属性を使うことでブラウザに読み込みの優先度を指定できます。 タグだけでなくfetch関数でもオプションでimportanceを指定できたりします。 imgやiframeタグにloading属性を追加する imgやiframeタグにはloading属性を使うことで読み込みのタイミングを指定できます。 もし、遅延/非同期読み込みしたい場合はloading='async'を使うと良いでしょう。 ただし、ファーストビューに使うと返って読み込みが遅くなる可能性もあるので注意しましょう。 imgタグにdecoding属性を追加する imgタグはdecoding属性を使うことでデコードを同期/非同期的に読み込むかを指定できます。 decoding='async'を指定すれば非同期的にデコード処理をブラウザに指示できます。 imgタグにはサイズを指定しておく imgタグのwidth/height属性などを使って、画像のサイズを指定しておきましょう。ブラウザのレンダリングの助けになります。 CLSの改善にも繋がります。 わからない場合は大体のサイズを指定しましょう。 優先度の高いリソースはlinkタグにpreloadを指定する ファーストビューに表示する画像など、優先度の高いリソースはlinkタグのrel属性にpreloadを指定ことで速い読み込みが期待できます。 優先度の高い外部ドメインへのアクセスがある時はlinkタグにdns-prefetchまたはpreconnectを指定する 外部ドメインからリソースを取得したり重要度の高い外部リンクを設置している場合などは、linkタグのdns-prefetchやpreconnectが使えます。 dns-prefetchはDNSルックアップ、preconnectは事前接続まで行います。 かなり優先度の高い外部ドメインへのアクセスはpreconnect、少し優先度が落ちる場合はdns-prefetchを使うと良いでしょう。 ユーザーがよく遷移するページはlinkタグにprerenderを指定する linkタグのrel属性にprerenderを指定することで、ブラウザは指定されたページをバック グラウンドでレンダリングします。 なので、ユーザーが指定されたページへ遷移する時はすぐに画面表示ができます。 ユースケースとしては、ランキングサイトのようなページで1位へのページへ遷移するユーザーは多いので、prerenderを指定しておくと良いかもしれません。 ただし、レンダリングされる都合上、ブラウザへの負荷が高かったり、JavaScriptで仕込んでいる計測処理が発火するなどの注意は必要です。 scriptタグにdeferやasync属性を追加する ブラウザでスクリプトが読み込まれるとHTMLやCSSの解析がブロックされます。 このような問題を解決するためにdeferやasync属性が使えます。 deferはHTMLやCSSの解析をブロックすることなくスクリプトを読み込んでおき、解析が完了したらスクリプトを実行します。 asyncはHTMLやCSSの解析とは独立してスクリプトの読み込み・実行をします。 Qiitaのこの記事がわかりやすいです。 優先度の高いリソースの読み込みはできるだけHTML上部で定義する ブラウザはHTMLドキュメントの上から解釈してきます。 なので、例えば同じpreloadを指定しているリソースでも、さらに優先度の高いものはよりHTML上部に定義して早めにブラウザが読み込めるようにしましょう。 CSSで余計なセレクタは書かない ブラウザはCSSセレクタを右から左に解析します。 なので、できる限り単一のクラス名やid名で指定した方が解析のスピードが上がります。 /* ブラウザは全てのdivタグを探し、さらに上の階層のhogeクラスを見つけようと解析する */ .hoge div {} /* Best Practice */ .hoge {} #hoge {} style属性を使って直接スタイルを指定する クラスなどセレクタを指定してCSSを書くよりも、直接HTMLタグのstyle属性を使ったほうがブラウザの解析は速いです。 ただし、コードの可読性やメンテが厳しくはなります。 <div style='color: red;'>ほげ</div> 不要なCSSを削除する 使っていないCSSは削除しましょう。 Chromeのデベロッパーツールを使えば不要なCSSを洗い出すことができます。 不要なJavaScriptを削除する 使っていないJavaScriptは削除しましょう。 例えば、console.logは基本的にプロダクションのコードでは不要なので、eslintで検出するなりbabelで削除するなりします。 ファーストビューに影響のあるCSSはheadタグの先頭で読み込む JavaScriptと違い、ブラウザのCSSの解析はHTMLの解析をブロックしません。 ファーストビューで読み込ませたいCSSはできるだけheadタグの先頭に読み込ませて、速くスタイリングされたファーストビューをユーザーに見せるようにしましょう。 ファーストビューに影響のないCSSはbodyタグの末尾で読み込む 逆にファーストビューに影響のないCSSはbodyタグの末尾で読み込ませることで、ブラウザにCSSの読み込みを遅延させます。 JavaScriptはbodyタグの末尾で読み込む ブラウザはJavaScriptの解析を始めるとHTMLやCSSの解析をストップします。 なので、JavaScriptはbodyタグの末尾で読み込み、HTMLやCSSの解析が終わった後のJavaScriptを解析するようにしましょう。 ただし、Google Analyticsなどの解析用のJavaScript等は除きます。 HTMLやCSS、JSをMinify/バンドルする Webpackやswcなどのバンドラーを使いましょう。 JavaScriptのトランスパイルを最新のESに合わせる もしJavaScriptをES2015でトランスパイルしている場合は、それよりも最新のバージョンでトランスパイルすることによって、JavaScriptのサイズを落とすことができます。 ただし、IEといった古いブラウザを切り捨てる覚悟は必要です。 画像はWebPやAVIFを使う 次世代の画像フォーマットとしてWebPやAVIFがあります。 こららの画像フォーマットを使うことで従来のPNG等の形式よりも画像サイズを縮小できたりします。 IKEAではAVIFによって画像の転送量を21.4%削減した例もあります。 画像サイズを縮小する 画質を落とすなり幅/高さを小さくするなりして画像サイズを縮小させます。 例えば、SVGでは作成したツールによってはコメントアウトが残っていたりで最適化されずに出力されている場合もあるので、手動で削除するなりツールを使うなりで縮小させます。 画像をインライン化する インライン画像としてHTMLに直接埋め込むことで、画像のリクエスト数を抑えることができます。 ただし、画像サイズが大きくなったりブラウザのキャッシュが効かない等のデメリットはあります。 画像サイズが小さく、一度しか読み込まれない場合などに有効といわれています。 過大なDOMを避ける DOMが多すぎるとブラウザの描画に負担をかけてしまいます。 不要なDOMを削除するのはもちろん、遅延読み込みや仮想無限スクロールなどを駆使してユーザーに表示されている部分だけ描画することで対策できます。 ブラウザAPI編 永続化ストレージはLocalStorageよりIndexedDBを使う ブラウザの永続化ストレージにはLocalStorageとIndexedDBが使えます。 LocalStorageは同期的、IndexedDBは非同期処理なので、IndexedDBの方がブラウザの動きを阻害することなくデータアクセスができます。 重たい処理やUIに依存しない処理はWebWorkerを使う WebWorkerを使うことでブラウザのメインスレッドとは別のスレッド立ち上げることができます。 フロントで検索機能といった重たい処理だったり、エラーをサーバーに送信するといったUIに依存しない処理はWebWorkerを使うことでメインスレッドの処理を阻害させません。 ServiceWorkerでリソースをキャッシュする ServiceWorker といえばPWA(Progressive Web Application)のイメージが強いですが、ブラウザから外部サーバーへのリクエストをフックしてHTMLやCSS、JSなどのリソースをキャッシュすることができます。 リクエストする際はキャッシュから取得することができるので外部サーバーへのリクエストするよりも処理が速くなります。 また、キャッシュから取得するか、先にサーバーへデータ取得してからキャッシュするかなど柔軟なキャッシュ戦略を選択できます。 ServiceWorkerを使う時はNavigationPreloadsも使う サイトにアクセス時、必要なリソースをフェッチする時にはServiceWorkerが起動するのを待ってフェッチ処理が走ります。 NavigationPreloadsではServiceWorkerの起動を待たずフェッチ処理を開始することができます。 WebAssembly を使う JavaScriptだけでなく、CやRustで書いたコードがブラウザで実行でき、JavaScriptよりも高速化される場合があります。 Amazonの事例もあります。 V8エンジン編 ChromeやNode.jsでは内部的にV8エンジンが使われています。 ここまで最適化すると変態ですが、チップスとして紹介します。 値の格納はコンストラクタで V8エンジンでは内部的にhidden classというものを生成します。 詳しい仕組みは割愛しますが、インスタンス化したオブジェクトに対して値を追加すると、新しいhidden classが生成されてしまいます。 class Point { constructor(x, y) { this.x = x; this.y = y; } } var p1 = new Point(11, 22); // hidden class の生成 var p2 = new Point(33, 44); // hidden class の再利用 p1.z = 55; // hidden class が生成されてしまう オブジェクトは同じ順番のプロパティで生成する これもhidden classに関わる話ですが、違う順番でプロパティを生成すると新たにhidden classが生成されます。 const obj = { a: 1 }; obj.b = 2 // hidden classを使い回せる const obj2 = { a: 1 }; obj2.b = 2 // 新しいhidden classが生成されてしまう const obj3 = { b: 2 }; obj3.a = 1 関数は同じ引数の型を使う 関数の引数はできるだけ同じ型を使うようにします。 function add(x,y) { return x + y } add(1,2) add(3,4) // OK add('3','4') // NG クラスはトップレベルのスコープで定義する 関数内でクラスを定義するのはV8エンジン的には良くないそうです。 // NG function createPoint(x, y) { class Point { constructor(x,y) { this.x = x this.y = y } } return new Point(x,y) } ライブラリ編 軽量なライブラリを採用する ライブラリを採用する1つの観点としてサイズがあります。 bundlephobia というサイトでライブラリのサイズをチェックすることができます。 ライブラリのサイズを減らす moment.js や lodash などのライブラリはWebpackのプラグインを使って不必要なスクリプトを削減することができます。 ライブラリのドキュメントを読む ライブラリの公式ドキュメントには最適化のTipsが載っていたりします。 例えば、Reactにはパフォーマンス最適化、TailwindCSSにはOptimizing for Productionというページが公式のドキュメントに記載されています。 各ライブラリのドキュメントをしっかり見てみましょう。 ライブラリに頼らず自前で作る ライブラリは万人向けに最適化されており、あなたのアプリケーション向けには最適化されていません。 あなたのアプリケーション以上に機能過多であることがほとんどです。 時には自前で作るのも1つの手です。 SPA編 ReactやVueといったコンポーネント志向のライブラリを想定しています。 Reactのコード例が多いですが、Vueでも参考になるかと思います。 コンポーネントがマウントされた後、遅延的にデータを読み込みする 優先順位だったりデータサイズが大きい場合等はマウント後リソースを取得します。 // 先にimportしない // import articles from './articles.json'; function ArticlesComponent() { const [articles, setArticles] = useState([]); // マウント後にデータを読み込む useEffect(() => { import('./articles.json').then(res => setArticles(res.default)); }, []) return articles.map(article => <div key={article.id}>{article.title}</div>) } コンポーネントを遅延読み込みする 初めてコンポーネントが表示されるタイミングでコンポーネントを読み込みます。 例えば、ユーザーがボタンをタップして初めて表示されるコンポーネントは遅延読み込みでの実装を考えます。 Reactで言えばSuspense、Next.jsならdyamicのAPIを使ってコンポーネントの遅延読み込みを実装できます。 SSRやSSG、ISRに移行する ReactやVueなど通常のSPAは性質上、初期描画が遅くなります。 ReactであればNext.jsやGatsuby.js、VueであればNuxt.jsといったフレームワークを使えば初期描画が遅くなる問題を解決できます。 コンポーネントの設計を最適化する ReactやVueだとコンポーネントのレンダリングの仕組みが違うので一概にこれが最適とは言えませんが、共通した設計の最適化があります。 例えば、「コンポーネントとデータの依存を考えて、再レンダリングの範囲を最小限にする」ことでしょう。 次のコンポーネントの例を見てください。 <!-- とあるコンポーネント --> <div> <div>データAに依存するUI部分</div> <div>データAに依存しないUI部分</div> </div> 1つのコンポーネント内に「データAに依存するUI部分」と「データAに依存しないUI部分」があります。 ReactであれVueであれこのようなケースの場合は「データAに依存しないUI部分」を別コンポーネントに切り出したほうが良いでしょう。 そうすればデータAに変更があった時、「データAに依存するUI部分」のみ再レンダリングさせることができます。 (Vueであれば問題ないですが、Reactの場合はステート管理のライブラリを使っていない場合はReact.memoを使う必要はあります) サーバー編 必要なデータのみフロントへ返却する 例えば、記事の一覧ページに各記事の本文を一部表示するとします。 「本文を一部」だけならサーバーからは一部だけ返却するようにします。 そうすることでファイルサイズ削減などができます。 事前に静的ファイルにしておく 都度APIへアクセスするのであれば予めJsonにしておくのも良いでしょう。 日本にあるサーバーを使う 日本向けのアプリを開発しているのであれば、地理的に近い日本のサーバーを選びましょう。 Brotli圧縮を使う gzipよりは圧縮後のサイズ削減や圧縮速度の向上が見込めます。 CDNを使う Amazon CloudFrontなどのCDNはできるなら使いましょう。 HTTP/2を使う できるなら使いましょう。HTTP/1.1より速いです。 HTTPキャッシュを使う Cache-ControlなどのHTTPヘッダーを利用して、ブラウザにリソースをキャッシュさせます。 まとめ この記事では次のようにカテゴリ分けしてWebフロントエンドのパフォーマンスチューニングのハウツーを紹介しました。 JavaScript編 HTML/CSSなどリソース編 ブラウザAPI編 V8エンジン編 ライブラリ編 SPA編 皆さんのパフォーマンスチューニング力の力添えになれば幸いです!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Webフロントエンドパフォーマンスチューニング55選

こんにちは、ぬこすけです。 近年、Webフロントエンドではサイトのパフォーマンスの重要性が高まっています。 例えば、GoogleはCore Web Vitalというパフォーマンスに指標を検索結果のランキング要因に組み込みました。 また、近年の某企業が「パフォーマンスの改善に取り組んだ結果、セッション数〇%アップ、CVR〇%アップ...」などの事例は枚挙にいとまがないでしょう。 パフォーマンスチューニングするためには、定量的に計測してボトルネックを探すようなトップダウンなアプローチもあります。 しかしながら、時には千本ノック的にハウツーを片っ端から試していくボトムアップなアプローチも有効になることもあったり、日々のコーディングでパフォーマンスを意識したコードを書くことは大切でしょう。 この記事ではパフォーマンス最適化のハウツーを紹介します。 パフォーマンス改善の施策が思い浮かばない時やフロントエンドのスキルを磨きたい時に辞書的な役割を果たせれば良いかなーと思っています。 ※私は55選書いている所で燃え尽きました。私自身も今後も更新する予定ですが、この記事は皆さんで作り上げたいと思っています。誤りや他にもこういうのあるよ!、この記事にわかりやすい説明あるよ!などあれば編集リクエスト、またはコメントいただけると幸いです。 ※この記事を読んでいる方にはこれからフロントエンジニアになりたい方、駆け出しエンジニアの方もいると思います。正直、何言ってるかわからない部分が結構あると思います。ですが、私の経験則上、「あの時書いてあったことはこういうことか!」と後々になって理解することがよくありました。今はよくわからないかもしれませんが、とりあえずストックなりしておいて、数ヶ月後にこの記事を見返すとまた理解度も変わるのかなーと思います。 注意事項 一口にフロントエンドといっても、SSRやらSSGやらでサーバー側も関わってくることもあるので、バックエンド寄りも話も混じっているので悪しからず。 わかりやすくするためにカテゴリに分けしていますが、微妙なカテゴリ分けのものもあるので悪しからず。 中には具体的なハウツーというより考え方みたいなものも混じっているかもしれませんが悪しからず。 環境によって必ずしもパフォーマンスが改善されるとは限らないので悪しからず。 あくまでパフォーマンスの観点なので他の観点では最適となるとは限らないので悪しからず。例えば、IndexedDBを紹介していますが、Sarafi 15で脆弱性が見つかっています。 紹介するものには特定のブラウザでしかサポートされていないものもあるので悪しからず。 JavaScript編 複数の非同期処理はPromise.allを使う もし互いに依存関係のない複数の非同期処理を実行しているのならば、Promise.allを使うのも手です。 async function notUsePromiseAll() { console.log('Start!!'); const response1 = await fetch("https://example.com/api/1"); const response2 = await fetch("https://example.com/api/2"); console.log('End!!'); } async function usePromiseAll() { console.log('Start!!'); const [response1, response2] = await Promise.all([ fetch("https://example.com/api/1"), fetch("https://example.com/api/2"), ]); console.log('End!!'); } Promise.allはいずれかの非同期処理が失敗すると全ての非同期処理が中断されます。 中断されたくない場合はPromise.allSettledが使えます。 非同期処理を待たなくて良い場合は待たない コードを眺めてみて、非同期処理を待たなくて良いところは待たないようにしましよう。 具体的には、もしasync/await構文を使っているならawaitを使わないことです。 const sendErrorToServer = async (message) => { // サーバーにエラー情報を送る処理 }; console.log('何かエラーが起きた'); // 後続の処理はサーバーにエラー情報を送る処理とは関係ないので await をつけない sendErrorToServer('エラーです'); console.log('後続の処理'); 先に非同期処理を走らせておく 互いに依存関係のある複数の非同期処理を実行する場合でも、時間がかかる処理の方を先に走らせておくのも良いでしょう。 const response1Promise = requestLongTime(); // ... // 色々処理 // ... const response1 = await response1Promise; const response2 = await requestShortTime(); console.log(response1, response2); キー/バリューを頻繁に追加や削除する場合はMapを使う MDNにも記載がありますが、キー/バリューのペアを頻繁に追加や削除する場合はObjectよりもMapを使ったほうが最適です。 const nameAgeMap = new Map() nameAgeMap.set('Tom', 19) nameAgeMap.set('Nancy', 32) nameAgeMap.delete('Tom') nameAgeMap.delete('Nancy') ... 膨大な配列の検索はキー/バリューで JavaScriptというよりかはロジックの問題かもしれません。 膨大な配列を検索する場合はキー/バリューに変換してから検索した方が速いです。 const thousandsPeople = [ { name: 'Tom', age: 19 }, { name: 'Nancy', age: 32 }, // ...めちゃくちゃ多い ] // 時間かかる const myFriend = thousandsPeople.find(({ name }) => name === 'Tom'); console.log(`The age is ${myFriend.age}`); const thousandsPeopleMap = { 'Tom': 19, 'Nancy': 32, // ... } // こっちのほうが速い const myFriend2 = thousandsPeopleMap['Tom']; console.log(`The age is ${myFriend2.age}`); 関数の結果をキャッシュする 頻繁に同じ引数で関数を実行したり、重い処理を走らせるなら関数の結果をキャッシュするのも有効です。 次のようなデコレータ関数を作れば、関数の結果をキャッシュできます。 function cachingDecorator(func) { const cache = new Map(); return x => { if (!x) { return func(x) } if (cache.has(x)) { return cache.get(x); } const result = func(x); cache.set(x, result); return result; } } function heavyFuncNoCache(str) { // 重い処理 } const heavyFunc = cachingDecorator(heavyFuncNoCache); heavyFunc('hoge'); // キャッシュから結果が返却される heavyFunc('hoge'); requireではなくimportを使う JavaScriptのモジュールの読み込み方にはrequireとimportの2種類があります。 requireは同期的、importは非同期的にモジュールを読み込むので、importの方が良いでしょう。 Node.jsといったサーバーサイドでJavaScriptを記述する場合はrequireを使うことが多いと思いますが、バージョン14であればpackage.jsonだったりファイルの拡張子をmjsにしたりいじることでimportで読み込めます。 なお、Qiitaのこの記事がわかりやすいです。 フェッチにはKeep-Aliveを指定する 何度も同じドメインへアクセスするのであればkeep-aliveを指定することでフェッチ処理が短縮されます。 import axios from 'axios'; import { Agent as HttpAgent } from 'http'; import { Agent as HttpsAgent } from 'https'; const httpAgent = new HttpAgent({ keepAlive: true }); const httpsAgent = new HttpsAgent({ keepAlive: true }); const keepAliveAxios = axios.create({ httpAgent, httpsAgent, }); keepAliveAxios.get(...); 非同期の関数を使う Node.jsには同期/非同期で別で用意されている関数があったりします。 例えばファイルに書き込みをする関数にはfs.writeFileSyncとfs.writeFileがあります。 もしフロントエンドアプリケーションのビルド時などに静的ファイルを生成する必要がある場合、特段理由がなければfs.writeFileを使いましょう。 HTML/CSSなどリソース編 imgやiframe、linkタグなどにimportance属性を追加する imgやiframe、linkタグなどではimportance属性を使うことでブラウザに読み込みの優先度を指定できます。 タグだけでなくfetch関数でもオプションでimportanceを指定できたりします。 imgやiframeタグにloading属性を追加する imgやiframeタグにはloading属性を使うことで読み込みのタイミングを指定できます。 もし、遅延/非同期読み込みしたい場合はloading='async'を使うと良いでしょう。 ただし、ファーストビューに使うと返って読み込みが遅くなる可能性もあるので注意しましょう。 imgタグにdecoding属性を追加する imgタグはdecoding属性を使うことでデコードを同期/非同期的に読み込むかを指定できます。 decoding='async'を指定すれば非同期的にデコード処理をブラウザに指示できます。 imgタグにはサイズを指定しておく imgタグのwidth/height属性などを使って、画像のサイズを指定しておきましょう。ブラウザのレンダリングの助けになります。 CLSの改善にも繋がります。 わからない場合は大体のサイズを指定しましょう。 優先度の高いリソースはlinkタグにpreloadを指定する ファーストビューに表示する画像など、優先度の高いリソースはlinkタグのrel属性にpreloadを指定ことで速い読み込みが期待できます。 優先度の高い外部ドメインへのアクセスがある時はlinkタグにdns-prefetchまたはpreconnectを指定する 外部ドメインからリソースを取得したり重要度の高い外部リンクを設置している場合などは、linkタグのdns-prefetchやpreconnectが使えます。 dns-prefetchはDNSルックアップ、preconnectは事前接続まで行います。 かなり優先度の高い外部ドメインへのアクセスはpreconnect、少し優先度が落ちる場合はdns-prefetchを使うと良いでしょう。 ユーザーがよく遷移するページはlinkタグにprerenderを指定する linkタグのrel属性にprerenderを指定することで、ブラウザは指定されたページをバック グラウンドでレンダリングします。 なので、ユーザーが指定されたページへ遷移する時はすぐに画面表示ができます。 ユースケースとしては、ランキングサイトのようなページで1位へのページへ遷移するユーザーは多いので、prerenderを指定しておくと良いかもしれません。 ただし、レンダリングされる都合上、ブラウザへの負荷が高かったり、JavaScriptで仕込んでいる計測処理が発火するなどの注意は必要です。 scriptタグにdeferやasync属性を追加する ブラウザでスクリプトが読み込まれるとHTMLやCSSの解析がブロックされます。 このような問題を解決するためにdeferやasync属性が使えます。 deferはHTMLやCSSの解析をブロックすることなくスクリプトを読み込んでおき、解析が完了したらスクリプトを実行します。 asyncはHTMLやCSSの解析とは独立してスクリプトの読み込み・実行をします。 Qiitaのこの記事がわかりやすいです。 優先度の高いリソースの読み込みはできるだけHTML上部で定義する ブラウザはHTMLドキュメントの上から解釈してきます。 なので、例えば同じpreloadを指定しているリソースでも、さらに優先度の高いものはよりHTML上部に定義して早めにブラウザが読み込めるようにしましょう。 CSSで余計なセレクタは書かない ブラウザはCSSセレクタを右から左に解析します。 なので、できる限り単一のクラス名やid名で指定した方が解析のスピードが上がります。 /* ブラウザは全てのdivタグを探し、さらに上の階層のhogeクラスを見つけようと解析する */ .hoge div {} /* Best Practice */ .hoge {} #hoge {} style属性を使って直接スタイルを指定する クラスなどセレクタを指定してCSSを書くよりも、直接HTMLタグのstyle属性を使ったほうがブラウザの解析は速いです。 ただし、コードの可読性やメンテが厳しくはなります。 <div style='color: red;'>ほげ</div> 不要なCSSを削除する 使っていないCSSは削除しましょう。 Chromeのデベロッパーツールを使えば不要なCSSを洗い出すことができます。 不要なJavaScriptを削除する 使っていないJavaScriptは削除しましょう。 例えば、console.logは基本的にプロダクションのコードでは不要なので、eslintで検出するなりbabelで削除するなりします。 ファーストビューに影響のあるCSSはheadタグの先頭で読み込む JavaScriptと違い、ブラウザのCSSの解析はHTMLの解析をブロックしません。 ファーストビューで読み込ませたいCSSはできるだけheadタグの先頭に読み込ませて、速くスタイリングされたファーストビューをユーザーに見せるようにしましょう。 ファーストビューに影響のないCSSはbodyタグの末尾で読み込む 逆にファーストビューに影響のないCSSはbodyタグの末尾で読み込ませることで、ブラウザにCSSの読み込みを遅延させます。 JavaScriptはbodyタグの末尾で読み込む ブラウザはJavaScriptの解析を始めるとHTMLやCSSの解析をストップします。 なので、JavaScriptはbodyタグの末尾で読み込み、HTMLやCSSの解析が終わった後のJavaScriptを解析するようにしましょう。 ただし、Google Analyticsなどの解析用のJavaScript等は除きます。 HTMLやCSS、JSをMinify/バンドルする Webpackやswcなどのバンドラーを使いましょう。 JavaScriptのトランスパイルを最新のESに合わせる もしJavaScriptをES2015でトランスパイルしている場合は、それよりも最新のバージョンでトランスパイルすることによって、JavaScriptのサイズを落とすことができます。 ただし、IEといった古いブラウザを切り捨てる覚悟は必要です。 画像はWebPやAVIFを使う 次世代の画像フォーマットとしてWebPやAVIFがあります。 こららの画像フォーマットを使うことで従来のPNG等の形式よりも画像サイズを縮小できたりします。 IKEAではAVIFによって画像の転送量を21.4%削減した例もあります。 画像サイズを縮小する 画質を落とすなり幅/高さを小さくするなりして画像サイズを縮小させます。 例えば、SVGでは作成したツールによってはコメントアウトが残っていたりで最適化されずに出力されている場合もあるので、手動で削除するなりツールを使うなりで縮小させます。 画像をインライン化する インライン画像としてHTMLに直接埋め込むことで、画像のリクエスト数を抑えることができます。 ただし、画像サイズが大きくなったりブラウザのキャッシュが効かない等のデメリットはあります。 画像サイズが小さく、一度しか読み込まれない場合などに有効といわれています。 過大なDOMを避ける DOMが多すぎるとブラウザの描画に負担をかけてしまいます。 不要なDOMを削除するのはもちろん、遅延読み込みや仮想無限スクロールなどを駆使してユーザーに表示されている部分だけ描画することで対策できます。 ブラウザAPI編 永続化ストレージはLocalStorageよりIndexedDBを使う ブラウザの永続化ストレージにはLocalStorageとIndexedDBが使えます。 LocalStorageは同期的、IndexedDBは非同期処理なので、IndexedDBの方がブラウザの動きを阻害することなくデータアクセスができます。 重たい処理やUIに依存しない処理はWebWorkerを使う WebWorkerを使うことでブラウザのメインスレッドとは別のスレッド立ち上げることができます。 フロントで検索機能といった重たい処理だったり、エラーをサーバーに送信するといったUIに依存しない処理はWebWorkerを使うことでメインスレッドの処理を阻害させません。 ServiceWorkerでリソースをキャッシュする ServiceWorker といえばPWA(Progressive Web Application)のイメージが強いですが、ブラウザから外部サーバーへのリクエストをフックしてHTMLやCSS、JSなどのリソースをキャッシュすることができます。 リクエストする際はキャッシュから取得することができるので外部サーバーへのリクエストするよりも処理が速くなります。 また、キャッシュから取得するか、先にサーバーへデータ取得してからキャッシュするかなど柔軟なキャッシュ戦略を選択できます。 ServiceWorkerを使う時はNavigationPreloadsも使う サイトにアクセス時、必要なリソースをフェッチする時にはServiceWorkerが起動するのを待ってフェッチ処理が走ります。 NavigationPreloadsではServiceWorkerの起動を待たずフェッチ処理を開始することができます。 WebAssembly を使う JavaScriptだけでなく、CやRustで書いたコードがブラウザで実行でき、JavaScriptよりも高速化される場合があります。 Amazonの事例もあります。 V8エンジン編 ChromeやNode.jsでは内部的にV8エンジンが使われています。 ここまで最適化すると変態ですが、チップスとして紹介します。 値の格納はコンストラクタで V8エンジンでは内部的にhidden classというものを生成します。 詳しい仕組みは割愛しますが、インスタンス化したオブジェクトに対して値を追加すると、新しいhidden classが生成されてしまいます。 class Point { constructor(x, y) { this.x = x; this.y = y; } } var p1 = new Point(11, 22); // hidden class の生成 var p2 = new Point(33, 44); // hidden class の再利用 p1.z = 55; // hidden class が生成されてしまう オブジェクトは同じ順番のプロパティで生成する これもhidden classに関わる話ですが、違う順番でプロパティを生成すると新たにhidden classが生成されます。 const obj = { a: 1 }; obj.b = 2 // hidden classを使い回せる const obj2 = { a: 1 }; obj2.b = 2 // 新しいhidden classが生成されてしまう const obj3 = { b: 2 }; obj3.a = 1 関数は同じ引数の型を使う 関数の引数はできるだけ同じ型を使うようにします。 function add(x,y) { return x + y } add(1,2) add(3,4) // OK add('3','4') // NG クラスはトップレベルのスコープで定義する 関数内でクラスを定義するのはV8エンジン的には良くないそうです。 // NG function createPoint(x, y) { class Point { constructor(x,y) { this.x = x this.y = y } } return new Point(x,y) } ライブラリ編 軽量なライブラリを採用する ライブラリを採用する1つの観点としてサイズがあります。 bundlephobia というサイトでライブラリのサイズをチェックすることができます。 ライブラリのサイズを減らす moment.js や lodash などのライブラリはWebpackのプラグインを使って不必要なスクリプトを削減することができます。 ライブラリのドキュメントを読む ライブラリの公式ドキュメントには最適化のTipsが載っていたりします。 例えば、Reactにはパフォーマンス最適化、TailwindCSSにはOptimizing for Productionというページが公式のドキュメントに記載されています。 各ライブラリのドキュメントをしっかり見てみましょう。 ライブラリに頼らず自前で作る ライブラリは万人向けに最適化されており、あなたのアプリケーション向けには最適化されていません。 あなたのアプリケーション以上に機能過多であることがほとんどです。 時には自前で作るのも1つの手です。 SPA編 ReactやVueといったコンポーネント志向のライブラリを想定しています。 Reactのコード例が多いですが、Vueでも参考になるかと思います。 コンポーネントがマウントされた後、遅延的にデータを読み込みする 優先順位だったりデータサイズが大きい場合等はマウント後リソースを取得します。 // 先にimportしない // import articles from './articles.json'; function ArticlesComponent() { const [articles, setArticles] = useState([]); // マウント後にデータを読み込む useEffect(() => { import('./articles.json').then(res => setArticles(res.default)); }, []) return articles.map(article => <div key={article.id}>{article.title}</div>) } コンポーネントを遅延読み込みする 初めてコンポーネントが表示されるタイミングでコンポーネントを読み込みます。 例えば、ユーザーがボタンをタップして初めて表示されるコンポーネントは遅延読み込みでの実装を考えます。 Reactで言えばSuspense、Next.jsならdyamicのAPIを使ってコンポーネントの遅延読み込みを実装できます。 SSRやSSG、ISRに移行する ReactやVueなど通常のSPAは性質上、初期描画が遅くなります。 ReactであればNext.jsやGatsuby.js、VueであればNuxt.jsといったフレームワークを使えば初期描画が遅くなる問題を解決できます。 コンポーネントの設計を最適化する ReactやVueだとコンポーネントのレンダリングの仕組みが違うので一概にこれが最適とは言えませんが、共通した設計の最適化があります。 例えば、「コンポーネントとデータの依存を考えて、再レンダリングの範囲を最小限にする」ことでしょう。 次のコンポーネントの例を見てください。 <!-- とあるコンポーネント --> <div> <div>データAに依存するUI部分</div> <div>データAに依存しないUI部分</div> </div> 1つのコンポーネント内に「データAに依存するUI部分」と「データAに依存しないUI部分」があります。 ReactであれVueであれこのようなケースの場合は「データAに依存しないUI部分」を別コンポーネントに切り出したほうが良いでしょう。 そうすればデータAに変更があった時、「データAに依存するUI部分」のみ再レンダリングさせることができます。 (Vueであれば問題ないですが、Reactの場合はステート管理のライブラリを使っていない場合はReact.memoを使う必要はあります) サーバー編 必要なデータのみフロントへ返却する 例えば、記事の一覧ページに各記事の本文を一部表示するとします。 「本文を一部」だけならサーバーからは一部だけ返却するようにします。 そうすることでファイルサイズ削減などができます。 事前に静的ファイルにしておく 都度APIへアクセスするのであれば予めJsonにしておくのも良いでしょう。 日本にあるサーバーを使う 日本向けのアプリを開発しているのであれば、地理的に近い日本のサーバーを選びましょう。 Brotli圧縮を使う gzipよりは圧縮後のサイズ削減や圧縮速度の向上が見込めます。 CDNを使う Amazon CloudFrontなどのCDNはできるなら使いましょう。 HTTP/2を使う できるなら使いましょう。HTTP/1.1より速いです。 HTTPキャッシュを使う Cache-ControlなどのHTTPヘッダーを利用して、ブラウザにリソースをキャッシュさせます。 まとめ この記事では次のようにカテゴリ分けしてWebフロントエンドのパフォーマンスチューニングのハウツーを紹介しました。 JavaScript編 HTML/CSSなどリソース編 ブラウザAPI編 V8エンジン編 ライブラリ編 SPA編 皆さんのパフォーマンスチューニング力の力添えになれば幸いです!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[React] 関数コンポーネントのpropsの渡し方と分割代入

概要 Reactの学習を進めるなかで、「関数コンポーネントでpropsを渡す際の記述が何通りかあるけど違いはなんだっけ?」と混乱したので整理します。 結論として、ここでは分割代入を使用しているパターンとそうでないパターンについて簡単にまとめます。 分割代入とは MDNによると 分割代入 (Destructuring assignment) 構文は、配列から値を取り出して、あるいはオブジェクトからプロパティを取り出して別個の変数に代入することを可能にする JavaScript の式です。 配列の要素やオブジェクトのプロパティを好きに取り出して、変数(定数)に代入することができます。 Reactでのpropsの渡し方 ここではnameとageをpropsとして渡す<Profile />コンポーネントを例にしてみます。 1. 「props.XX」と書く渡し方 まずは分割代入を使わない書き方だと下記のようになります。 各propsに対してprops.XXと記述する必要があり、数が多いと煩雑です。 export const Profile = (props) => { return ( <p>私の名前は{props.name}です。{props.age}歳です。</p> ) 2. propsを分割代入する渡し方 ここで<Profile />コンポーネントのpropsの中身をみてみます。 例えば、<Profile name="Yamada" age="26" />としたときのpropsは下記の通りです。 { name: "Yamada", age: "26" } このようにpropsはオブジェクトなので分割代入ができます。 というわけで、propsを分割代入すると以下のように書けます。 export const Profile = (props) => { // propsを分割代入 const { name, age } = props; return ( <p>`私の名前は{name}です。{age}歳です。`</p> ) 毎回props.XXと書く必要がなくなり、スッキリしましたね! 3. さらに引数の段階で分割代入 さらに、引数にpropsを渡す段階で分割代入すると以下のようになります。 // 引数にpropsを渡す段階で分割代入 export const Profile = ({ name, age }) => { return ( <p>`私の名前は{name}です。{age}歳です。`</p> ) propsのプロパティをオブジェクトとして直接渡している、ということですかね。 見た目的には一番スッキリ。 まとめ propsを渡す記述はいくつかありますが、どのパターンもやってることは一緒なので、プロジェクトのルールや好みで選択するのがよいかと思います。 記事を書いてみて 色んな記述を見て混乱していたのは「propsはオブジェクトである」ということを当初よく分かっていなかったののが大きいように思います。 公式ドキュメントにもこのように書いてありましたw React がユーザ定義のコンポーネントを見つけた場合、JSX に書かれている属性と子要素を単一のオブジェクトとしてこのコンポーネントに渡します。このオブジェクトのことを “props” と呼びます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む